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 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 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,24 +14,83 @@
{{.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}}
-