diff options
author | John Olheiser <john.olheiser@gmail.com> | 2020-08-27 20:36:37 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-28 09:36:37 +0800 |
commit | 211321fb936683815c4033fdfb9ac46af8a3b357 (patch) | |
tree | 13f356084062e1827bda3ba0d1c9f4b4b5c2c799 | |
parent | ed2f6e137be8fe3c85292e8344b5bb5cfb56891d (diff) | |
download | gitea-211321fb936683815c4033fdfb9ac46af8a3b357.tar.gz gitea-211321fb936683815c4033fdfb9ac46af8a3b357.zip |
Git migration UX (#12619)
* Initial work
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Implementation
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Fix gitlab and token cloning
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Imports and JS
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Fix test
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Linting
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Generate swagger
Signed-off-by: jolheiser <john.olheiser@gmail.com>
* Move mirror toggle and rename options
Signed-off-by: jolheiser <john.olheiser@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
-rw-r--r-- | modules/auth/repo_form.go | 2 | ||||
-rw-r--r-- | modules/migrations/base/downloader.go | 8 | ||||
-rw-r--r-- | modules/migrations/base/release.go | 2 | ||||
-rw-r--r-- | modules/migrations/base/uploader.go | 2 | ||||
-rw-r--r-- | modules/migrations/git.go | 6 | ||||
-rw-r--r-- | modules/migrations/gitea.go | 13 | ||||
-rw-r--r-- | modules/migrations/gitea_test.go | 2 | ||||
-rw-r--r-- | modules/migrations/github.go | 63 | ||||
-rw-r--r-- | modules/migrations/github_test.go | 2 | ||||
-rw-r--r-- | modules/migrations/gitlab.go | 57 | ||||
-rw-r--r-- | modules/migrations/gitlab_test.go | 2 | ||||
-rw-r--r-- | modules/migrations/migrate.go | 13 | ||||
-rw-r--r-- | modules/structs/repo.go | 27 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 10 | ||||
-rw-r--r-- | routers/repo/repo.go | 16 | ||||
-rw-r--r-- | templates/repo/migrate.tmpl | 126 | ||||
-rw-r--r-- | templates/swagger/v1_json.tmpl | 9 | ||||
-rw-r--r-- | web_src/js/features/migration.js | 53 | ||||
-rw-r--r-- | web_src/js/index.js | 28 | ||||
-rw-r--r-- | web_src/less/_form.less | 5 |
20 files changed, 269 insertions, 177 deletions
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 696d3b9a53..b3fead7da9 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 c31f3df1d1..b692969ba5 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 b2541f1bf5..2a223920c7 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 85ad60fe0e..07c2bb0d42 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 af345808b5..5c9acb2533 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 8c097e143c..082ddcd5fb 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 c0d2dcd180..02b2f0a5c9 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 97d62b994f..eb73a7e0d4 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 814c771e8c..0b8c559d30 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 4f218c95f1..eec16d2433 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 003da5bbdf..daf05f8e3a 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 c970ba6920..7858dfc685 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 217a6f74ad..808d2ffbc8 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 388348e95c..5aeffc7580 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 <span class="text blue">mirror</span> -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 <span class="text blue">mirror</span> +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 <a href="%[1]s">%[2]s</a> migrated_from_fake = Migrated From %[1]s migrate.migrating = Migrating from <b>%s</b> ... diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 27c8ff1e03..71df2d0cb7 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 60b432beaa..d5a31a6800 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -14,83 +14,52 @@ <input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> <span class="help"> {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} - <br/>{{.i18n.Tr "repo.migrate.migrate_items_options"}} {{if .LFSActive}}<br/>{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}} </span> </div> - <div class="ui accordion optional field"> - <div class="title {{if .Err_Auth}}text red active{{end}}"> - <i class="icon dropdown"></i> - {{.i18n.Tr "repo.need_auth"}} - </div> - <div class="content {{if .Err_Auth}}active{{end}}"> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_username">{{.i18n.Tr "username"}}</label> - <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> - </div> - <input class="fake" type="password"> - <div class="inline field {{if .Err_Auth}}error{{end}}"> - <label for="auth_password">{{.i18n.Tr "password"}}</label> - <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> - </div> - </div> - </div> - <div class="ui divider"></div> - - <div class="inline required field {{if .Err_Owner}}error{{end}}"> - <label>{{.i18n.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text" title="{{.ContextUser.Name}}"> - <img class="ui mini image" src="{{.ContextUser.RelAvatarLink}}"> - {{.ContextUser.ShortName 20}} - </span> + <div class="inline field"> + <label>{{.i18n.Tr "repo.migrate_service"}}</label> + <div class="ui selection dropdown"> + <input id="service_type" type="hidden" name="service" value="{{.service}}"> + <div class="default text"></div> <i class="dropdown icon"></i> - <div class="menu" title="{{.SignedUser.Name}}"> - <div class="item" data-value="{{.SignedUser.ID}}"> - <img class="ui mini image" src="{{.SignedUser.RelAvatarLink}}"> - {{.SignedUser.ShortName 20}} - </div> - {{range .Orgs}} - <div class="item" data-value="{{.ID}}" title="{{.Name}}"> - <img class="ui mini image" src="{{.RelAvatarLink}}"> - {{.ShortName 20}} - </div> + <div class="menu"> + {{range .Services}} + <div id="service-{{.}}" class="item" data-token="{{.TokenAuth}}" data-value="{{.}}">{{.Title}}</div> {{end}} </div> </div> </div> - - <div class="inline required field {{if .Err_RepoName}}error{{end}}"> - <label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label> - <input id="repo_name" name="repo_name" value="{{.repo_name}}" required> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_username">{{.i18n.Tr "username"}}</label> + <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> </div> - <div class="inline field"> - <label>{{.i18n.Tr "repo.visibility"}}</label> - <div class="ui checkbox"> - {{if .IsForcedPrivate}} - <input name="private" type="checkbox" checked readonly> - <label>{{.i18n.Tr "repo.visibility_helper_forced" | Safe}}</label> - {{else}} - <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{.i18n.Tr "repo.visibility_helper" | Safe}}</label> - {{end}} - </div> + <input class="fake" type="password"> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_password">{{.i18n.Tr "password"}}</label> + <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> + </div> + <div class="inline field {{if .Err_Auth}}error{{end}}"> + <label for="auth_token">{{.i18n.Tr "access_token"}}</label> + <input id="auth_token" name="auth_token" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> </div> + <div class="inline field"> - <label>{{.i18n.Tr "repo.migrate_type"}}</label> + <label>{{.i18n.Tr "repo.migrate_options"}}</label> <div class="ui checkbox"> {{if .DisableMirrors}} <input id="mirror" name="mirror" type="checkbox" readonly> - <label>{{.i18n.Tr "repo.migrate_type_helper_disabled"}}</label> + <label>{{.i18n.Tr "repo.migrate_options_mirror_disabled"}}</label> {{else}} <input id="mirror" name="mirror" type="checkbox" {{if .mirror}}checked{{end}}> - <label>{{.i18n.Tr "repo.migrate_type_helper" | Safe}}</label> + <label>{{.i18n.Tr "repo.migrate_options_mirror_helper" | Safe}}</label> {{end}} </div> </div> - <div id="migrate_items" class="ui field"> + + <span class="help">{{.i18n.Tr "repo.migrate.migrate_items_options"}}</span> + <div id="migrate_items"> <div class="inline field"> <label>{{.i18n.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> @@ -125,6 +94,49 @@ </div> </div> </div> + + <div class="ui divider"></div> + + <div class="inline required field {{if .Err_Owner}}error{{end}}"> + <label>{{.i18n.Tr "repo.owner"}}</label> + <div class="ui selection owner dropdown"> + <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> + <span class="text" title="{{.ContextUser.Name}}"> + <img class="ui mini image" src="{{.ContextUser.RelAvatarLink}}"> + {{.ContextUser.ShortName 20}} + </span> + <i class="dropdown icon"></i> + <div class="menu" title="{{.SignedUser.Name}}"> + <div class="item" data-value="{{.SignedUser.ID}}"> + <img class="ui mini image" src="{{.SignedUser.RelAvatarLink}}"> + {{.SignedUser.ShortName 20}} + </div> + {{range .Orgs}} + <div class="item" data-value="{{.ID}}" title="{{.Name}}"> + <img class="ui mini image" src="{{.RelAvatarLink}}"> + {{.ShortName 20}} + </div> + {{end}} + </div> + </div> + </div> + + <div class="inline required field {{if .Err_RepoName}}error{{end}}"> + <label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label> + <input id="repo_name" name="repo_name" value="{{.repo_name}}" required> + </div> + <div class="inline field"> + <label>{{.i18n.Tr "repo.visibility"}}</label> + <div class="ui checkbox"> + {{if .IsForcedPrivate}} + <input name="private" type="checkbox" checked readonly> + <label>{{.i18n.Tr "repo.visibility_helper_forced" | Safe}}</label> + {{else}} + <input name="private" type="checkbox" {{if .private}}checked{{end}}> + <label>{{.i18n.Tr "repo.visibility_helper" | Safe}}</label> + {{end}} + </div> + </div> <div class="inline field {{if .Err_Description}}error{{end}}"> <label for="description">{{.i18n.Tr "repo.repo_desc"}}</label> <textarea id="description" name="description">{{.description}}</textarea> diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d9c8aeb87d..f1e0b0080d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13446,6 +13446,10 @@ "type": "string", "x-go-name": "AuthPassword" }, + "auth_token": { + "type": "string", + "x-go-name": "AuthToken" + }, "auth_username": { "type": "string", "x-go-name": "AuthUsername" @@ -13490,6 +13494,11 @@ "type": "string", "x-go-name": "RepoName" }, + "service": { + "type": "integer", + "format": "int64", + "x-go-name": "Service" + }, "uid": { "type": "integer", "format": "int64", diff --git a/web_src/js/features/migration.js b/web_src/js/features/migration.js new file mode 100644 index 0000000000..e4c306307f --- /dev/null +++ b/web_src/js/features/migration.js @@ -0,0 +1,53 @@ +const $service = $('#service_type'); +const $user = $('#auth_username'); +const $pass = $('#auth_password'); +const $token = $('#auth_token'); +const $items = $('#migrate_items').find('.field'); + +export default function initMigration() { + checkAuth(); + + $service.on('change', checkAuth); + $user.on('keyup', () => {checkItems(false)}); + $pass.on('keyup', () => {checkItems(false)}); + $token.on('keyup', () => {checkItems(true)}); + + const $cloneAddr = $('#clone_addr'); + $cloneAddr.on('change', () => { + const $repoName = $('#repo_name'); + if ($cloneAddr.val().length > 0 && $repoName.val().length === 0) { // Only modify if repo_name input is blank + $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]); + } + }); +} + +function checkAuth() { + const serviceType = $service.val(); + const tokenAuth = $(`#service-${serviceType}`).data('token'); + + if (tokenAuth) { + $user.parent().addClass('disabled'); + $pass.parent().addClass('disabled'); + $token.parent().removeClass('disabled'); + } else { + $user.parent().removeClass('disabled'); + $pass.parent().removeClass('disabled'); + $token.parent().addClass('disabled'); + } + + checkItems(tokenAuth); +} + +function checkItems(tokenAuth) { + let enableItems; + if (tokenAuth) { + enableItems = $token.val() !== ''; + } else { + enableItems = $user.val() !== '' || $pass.val() !== ''; + } + if (enableItems && $service.val() > 1) { + $items.removeClass('disabled'); + } else { + $items.addClass('disabled'); + } +} diff --git a/web_src/js/index.js b/web_src/js/index.js index a1b5035764..810493a7d6 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -8,6 +8,7 @@ import {htmlEscape} from 'escape-goat'; import 'jquery.are-you-sure'; import './vendor/semanticdropdown.js'; +import initMigration from './features/migration.js'; import initContextPopups from './features/contextpopup.js'; import initGitGraph from './features/gitgraph.js'; import initClipboard from './features/clipboard.js'; @@ -1155,25 +1156,6 @@ async function initRepository() { } } -function initMigration() { - const toggleMigrations = function () { - const authUserName = $('#auth_username').val(); - const cloneAddr = $('#clone_addr').val(); - if (!$('#mirror').is(':checked') && (authUserName && authUserName.length > 0) && - (cloneAddr !== undefined && (cloneAddr.startsWith('https://github.com') || cloneAddr.startsWith('http://github.com') || cloneAddr.startsWith('http://gitlab.com') || cloneAddr.startsWith('https://gitlab.com')))) { - $('#migrate_items').show(); - } else { - $('#migrate_items').hide(); - } - }; - - toggleMigrations(); - - $('#clone_addr').on('input', toggleMigrations); - $('#auth_username').on('input', toggleMigrations); - $('#mirror').on('change', toggleMigrations); -} - function initPullRequestReview() { $('.show-outdated').on('click', function (e) { e.preventDefault(); @@ -2477,14 +2459,6 @@ $(document).ready(async () => { } } - const $cloneAddr = $('#clone_addr'); - $cloneAddr.on('change', () => { - const $repoName = $('#repo_name'); - if ($cloneAddr.val().length > 0 && $repoName.val().length === 0) { // Only modify if repo_name input is blank - $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]); - } - }); - // parallel init of async loaded features await Promise.all([ attachTribute(document.querySelectorAll('#content, .emoji-input')), diff --git a/web_src/less/_form.less b/web_src/less/_form.less index 07c7cdfc8b..f8123b8f9b 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -180,6 +180,11 @@ text-align: center; } + .selection.dropdown { + vertical-align: middle; + width: 50% !important; + } + @media only screen and (max-width: 768px) { label, input, |