* 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>tags/v1.13.0-rc1
@@ -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 |
@@ -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 | |||
} |
@@ -8,7 +8,7 @@ import "time" | |||
// ReleaseAsset represents a release asset | |||
type ReleaseAsset struct { | |||
URL string | |||
ID int64 | |||
Name string | |||
ContentType *string | |||
Size *int |
@@ -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 |
@@ -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 |
@@ -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 { |
@@ -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) | |||
) |
@@ -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{ |
@@ -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) | |||
@@ -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 |
@@ -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") | |||
} |
@@ -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:] |
@@ -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 |
@@ -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> ... |
@@ -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, |
@@ -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> |
@@ -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", |
@@ -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'); | |||
} | |||
} |
@@ -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')), |
@@ -180,6 +180,11 @@ | |||
text-align: center; | |||
} | |||
.selection.dropdown { | |||
vertical-align: middle; | |||
width: 50% !important; | |||
} | |||
@media only screen and (max-width: 768px) { | |||
label, | |||
input, |