Add a migrate service type switch page (#12697)

* Add a migrat service type switch page

* Improve translations

* remove images

* Fix images

* remove extra create repo button on dashboard

* Follow reviewers' opinions

* Fix frontend lint

* Remove wrong submit file

* Fix tests

* Adjust the size of image

* Apply suggestions from code review

Co-authored-by: 赵智超 <1012112796@qq.com>

* Remove username and password from migration of github/gitlab

* Improve docs

* Improve interface docs

Co-authored-by: 赵智超 <1012112796@qq.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
tokarchuk/v1.17
Lunny Xiao 4 years ago committed by GitHub
parent 6483dbc8ec
commit e153cf07c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      integrations/repo_migrate_test.go
  2. 13
      modules/structs/repo.go
  3. 7
      options/locale/locale_en-US.ini
  4. 1
      public/img/svg/gitea-git.svg
  5. 1
      public/img/svg/gitea-github.svg
  6. 1
      public/img/svg/gitea-gitlab.svg
  7. 173
      routers/repo/migrate.go
  8. 148
      routers/repo/repo.go
  9. 2
      routers/repo/view.go
  10. 103
      templates/repo/migrate/git.tmpl
  11. 26
      templates/repo/migrate/github.tmpl
  12. 137
      templates/repo/migrate/gitlab.tmpl
  13. 23
      templates/repo/migrate/migrate.tmpl
  14. 0
      templates/repo/migrate/migrating.tmpl
  15. 8
      templates/user/dashboard/repolist.tmpl
  16. 14
      web_src/js/features/migration.js
  17. 4
      web_src/less/_repository.less
  18. 16
      web_src/less/themes/theme-arc-green.less
  19. 1
      web_src/svg/gitea-git.svg
  20. 1
      web_src/svg/gitea-github.svg
  21. 1
      web_src/svg/gitea-gitlab.svg

@ -5,15 +5,17 @@
package integrations package integrations
import ( import (
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder { func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", "/repo/migrate") req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", structs.PlainGitService)) // render plain git migration page
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
@ -28,8 +30,8 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str
"clone_addr": cloneAddr, "clone_addr": cloneAddr,
"uid": uid, "uid": uid,
"repo_name": repoName, "repo_name": repoName,
}, "service": fmt.Sprintf("%d", structs.PlainGitService),
) })
resp = session.MakeRequest(t, req, http.StatusFound) resp = session.MakeRequest(t, req, http.StatusFound)
return resp return resp

@ -5,6 +5,7 @@
package structs package structs
import ( import (
"strings"
"time" "time"
) )
@ -205,17 +206,7 @@ const (
// Name represents the service type's name // Name represents the service type's name
// WARNNING: the name have to be equal to that on goth's library // WARNNING: the name have to be equal to that on goth's library
func (gt GitServiceType) Name() string { func (gt GitServiceType) Name() string {
switch gt { return strings.ToLower(gt.Title())
case GithubService:
return "github"
case GiteaService:
return "gitea"
case GitlabService:
return "gitlab"
case GogsService:
return "gogs"
}
return ""
} }
// Title represents the service type's proper title // Title represents the service type's proper title

@ -720,6 +720,7 @@ migrate_items_milestones = Milestones
migrate_items_labels = Labels migrate_items_labels = Labels
migrate_items_issues = Issues migrate_items_issues = Issues
migrate_items_pullrequests = Pull Requests migrate_items_pullrequests = Pull Requests
migrate_items_merge_requests = Merge Requests
migrate_items_releases = Releases migrate_items_releases = Releases
migrate_repo = Migrate Repository migrate_repo = Migrate Repository
migrate.clone_address = Migrate / Clone From URL migrate.clone_address = Migrate / Clone From URL
@ -729,11 +730,15 @@ 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.invalid_local_path = "The local path is invalid. It does not exist or is not a directory."
migrate.failed = Migration failed: %v 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.lfs_mirror_unsupported = Mirroring LFS objects is not supported - use 'git lfs fetch --all' and 'git lfs push --all' instead.
migrate.migrate_items_options = Authentication is needed to migrate items from a service that supports them. migrate.migrate_items_options = Access Token is required to migrate additional items
migrated_from = Migrated from <a href="%[1]s">%[2]s</a> migrated_from = Migrated from <a href="%[1]s">%[2]s</a>
migrated_from_fake = Migrated From %[1]s migrated_from_fake = Migrated From %[1]s
migrate.migrate = Migrate From %s
migrate.migrating = Migrating from <b>%s</b> ... migrate.migrating = Migrating from <b>%s</b> ...
migrate.migrating_failed = Migrating from <b>%s</b> failed. migrate.migrating_failed = Migrating from <b>%s</b> failed.
migrate.github.description = Migrating data from Github.com or Github Enterprise.
migrate.git.description = Migrating or Mirroring git data from Git services
migrate.gitlab.description = Migrating data from GitLab.com or Self-Hosted gitlab server.
mirror_from = mirror of mirror_from = mirror of
forked_from = forked from forked_from = forked from

@ -0,0 +1 @@
<svg viewBox="0 0 48 48" class="svg gitea-git" width="16" height="16" aria-hidden="true"><path fill="#F4511E" d="M42.2 22.1L25.9 5.8c-.5-.5-1.2-.8-1.9-.8s-1.4.3-1.9.8l-3.5 3.5 4.1 4.1c.4-.2.8-.3 1.3-.3 1.7 0 3 1.3 3 3 0 .5-.1.9-.3 1.3l4 4c.4-.2.8-.3 1.3-.3 1.7 0 3 1.3 3 3s-1.3 3-3 3-3-1.3-3-3c0-.5.1-.9.3-1.3l-4-4c-.1 0-.2.1-.3.1v10.4c1.2.4 2 1.5 2 2.8 0 1.7-1.3 3-3 3s-3-1.3-3-3c0-1.3.8-2.4 2-2.8V18.8c-1.2-.4-2-1.5-2-2.8 0-.5.1-.9.3-1.3l-4.1-4.1L5.8 22.1c-.5.5-.8 1.2-.8 1.9s.3 1.4.8 1.9l16.3 16.3c.5.5 1.2.8 1.9.8s1.4-.3 1.9-.8l16.3-16.3c.5-.5.8-1.2.8-1.9s-.3-1.4-.8-1.9z"/></svg>

After

Width:  |  Height:  |  Size: 584 B

@ -0,0 +1 @@
<svg viewBox="0 0 64 64" class="svg gitea-github" width="16" height="16" aria-hidden="true"><linearGradient id="a" x1="30.999" x2="30.999" y1="16" y2="55.342" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#6dc7ff"/><stop offset="1" stop-color="#e6abff"/></linearGradient><path fill="url(#a)" d="M25.008 56.007c-.003-.368-.006-1.962-.009-3.454l-.003-1.55c-6.729.915-8.358-3.78-8.376-3.83-.934-2.368-2.211-3.045-2.266-3.073l-.124-.072c-.463-.316-1.691-1.157-1.342-2.263.315-.997 1.536-1.1 2.091-1.082 3.074.215 4.63 2.978 4.694 3.095 1.569 2.689 3.964 2.411 5.509 1.844.144-.688.367-1.32.659-1.878-4.956-.879-10.571-3.515-10.571-13.104 0-2.633.82-4.96 2.441-6.929-.362-1.206-.774-3.666.446-6.765l.174-.442.452-.144c.416-.137 2.688-.624 7.359 2.433a24.959 24.959 0 016.074-.759c2.115.01 4.158.265 6.09.759 4.667-3.058 6.934-2.565 7.351-2.433l.451.145.174.44c1.225 3.098.813 5.559.451 6.766 1.618 1.963 2.438 4.291 2.438 6.929 0 9.591-5.621 12.219-10.588 13.087.563 1.065.868 2.402.868 3.878 0 1.683-.007 7.204-.015 8.402l-2-.014c.008-1.196.015-6.708.015-8.389 0-2.442-.943-3.522-1.35-3.874l-1.73-1.497 2.274-.253c5.205-.578 10.525-2.379 10.525-11.341 0-2.33-.777-4.361-2.31-6.036l-.43-.469.242-.587c.166-.401.894-2.442-.043-5.291-.758.045-2.568.402-5.584 2.447l-.384.259-.445-.123c-1.863-.518-3.938-.796-6.001-.806-2.052.01-4.124.288-5.984.806l-.445.123-.383-.259c-3.019-2.044-4.833-2.404-5.594-2.449-.935 2.851-.206 4.892-.04 5.293l.242.587-.429.469c-1.536 1.681-2.314 3.712-2.314 6.036 0 8.958 5.31 10.77 10.504 11.361l2.252.256-1.708 1.49c-.372.325-1.03 1.112-1.254 2.727l-.075.549-.506.227c-1.321.592-5.839 2.162-8.548-2.485-.015-.025-.544-.945-1.502-1.557.646.639 1.433 1.673 2.068 3.287.066.19 1.357 3.622 7.28 2.339l1.206-.262.012 3.978c.003 1.487.006 3.076.009 3.444l-1.998.014z"/><linearGradient id="b" x1="32" x2="32" y1="5" y2="59.167" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#b)" d="M32 58C17.663 58 6 46.337 6 32S17.663 6 32 6s26 11.663 26 26-11.663 26-26 26zm0-50C18.767 8 8 18.767 8 32s10.767 24 24 24 24-10.767 24-24S45.233 8 32 8z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1 @@
<svg viewBox="0 0 48 48" class="svg gitea-gitlab" width="16" height="16" aria-hidden="true"><path fill="#e53935" d="M24 43l-8-23h16z"/><path fill="#ff7043" d="M24 43l18-23H32z"/><path fill="#e53935" d="M37 5l5 15H32z"/><path fill="#ffa726" d="M24 43l18-23 3 8z"/><path fill="#ff7043" d="M24 43L6 20h10z"/><path fill="#e53935" d="M11 5L6 20h10z"/><path fill="#ffa726" d="M24 43L6 20l-3 8z"/></svg>

After

Width:  |  Height:  |  Size: 396 B

@ -0,0 +1,173 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/migrations"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/modules/util"
)
const (
tplMigrate base.TplName = "repo/migrate/migrate"
)
// Migrate render migration of repository page
func Migrate(ctx *context.Context) {
ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
serviceType := ctx.QueryInt("service_type")
if serviceType == 0 {
ctx.HTML(200, tplMigrate)
return
}
ctx.Data["Title"] = ctx.Tr("new_migrate")
ctx.Data["private"] = getRepoPrivate(ctx)
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors
ctx.Data["mirror"] = ctx.Query("mirror") == "1"
ctx.Data["wiki"] = ctx.Query("wiki") == "1"
ctx.Data["milestones"] = ctx.Query("milestones") == "1"
ctx.Data["labels"] = ctx.Query("labels") == "1"
ctx.Data["issues"] = ctx.Query("issues") == "1"
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.GitServiceType(serviceType)
ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
ctx.HTML(200, base.TplName("repo/migrate/"+structs.GitServiceType(serviceType).Name()))
}
func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) {
switch {
case migrations.IsRateLimitError(err):
ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case models.IsErrReachLimitOfRepo(err):
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
case models.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
case models.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
remoteAddr, _ := form.ParseRemoteAddr(owner)
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.Data["Err_Auth"] = true
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
} else if strings.Contains(err.Error(), "fatal:") {
ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
} else {
ctx.ServerError(name, err)
}
}
}
// 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"] = form.Service
ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
ctx.HTML(200, tplMigrate)
return
}
remoteAddr, err := form.ParseRemoteAddr(ctx.User)
if err != nil {
if models.IsErrInvalidCloneAddr(err) {
ctx.Data["Err_CloneAddr"] = true
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error"), tplMigrate, &form)
case addrErr.IsPermissionDenied:
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplMigrate, &form)
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplMigrate, &form)
default:
ctx.ServerError("Unknown error", err)
}
} else {
ctx.ServerError("ParseRemoteAddr", err)
}
return
}
var opts = migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
GitServiceType: structs.GitServiceType(form.Service),
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror && !setting.Repository.DisableMirrors,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
AuthToken: form.AuthToken,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: form.Issues || form.PullRequests,
PullRequests: form.PullRequests,
Releases: form.Releases,
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}
err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
return
}
err = task.MigrateRepository(ctx.User, ctxUser, opts)
if err == nil {
ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName)
return
}
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
}

@ -17,11 +17,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/migrations"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
"github.com/unknwon/com" "github.com/unknwon/com"
@ -29,7 +25,6 @@ import (
const ( const (
tplCreate base.TplName = "repo/create" tplCreate base.TplName = "repo/create"
tplMigrate base.TplName = "repo/migrate"
) )
// MustBeNotEmpty render when a repo is a empty git dir // MustBeNotEmpty render when a repo is a empty git dir
@ -254,149 +249,6 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) {
handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
} }
// Migrate render migration of repository page
func Migrate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_migrate")
ctx.Data["private"] = getRepoPrivate(ctx)
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors
ctx.Data["mirror"] = ctx.Query("mirror") == "1"
ctx.Data["wiki"] = ctx.Query("wiki") == "1"
ctx.Data["milestones"] = ctx.Query("milestones") == "1"
ctx.Data["labels"] = ctx.Query("labels") == "1"
ctx.Data["issues"] = ctx.Query("issues") == "1"
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() {
return
}
ctx.Data["ContextUser"] = ctxUser
ctx.HTML(200, tplMigrate)
}
func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) {
switch {
case migrations.IsRateLimitError(err):
ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case models.IsErrReachLimitOfRepo(err):
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
case models.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
case models.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
remoteAddr, _ := form.ParseRemoteAddr(owner)
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.Data["Err_Auth"] = true
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
} else if strings.Contains(err.Error(), "fatal:") {
ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
} else {
ctx.ServerError(name, err)
}
}
}
// 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() {
return
}
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
ctx.HTML(200, tplMigrate)
return
}
remoteAddr, err := form.ParseRemoteAddr(ctx.User)
if err != nil {
if models.IsErrInvalidCloneAddr(err) {
ctx.Data["Err_CloneAddr"] = true
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error"), tplMigrate, &form)
case addrErr.IsPermissionDenied:
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplMigrate, &form)
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplMigrate, &form)
default:
ctx.ServerError("Unknown error", err)
}
} else {
ctx.ServerError("ParseRemoteAddr", err)
}
return
}
var opts = migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
GitServiceType: structs.GitServiceType(form.Service),
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror && !setting.Repository.DisableMirrors,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
AuthToken: form.AuthToken,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: true,
PullRequests: form.PullRequests,
Releases: form.Releases,
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}
err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
return
}
err = task.MigrateRepository(ctx.User, ctxUser, opts)
if err == nil {
ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName)
return
}
handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
}
// Action response for actions to a repository // Action response for actions to a repository
func Action(ctx *context.Context) { func Action(ctx *context.Context) {
var err error var err error

@ -34,7 +34,7 @@ const (
tplRepoHome base.TplName = "repo/home" tplRepoHome base.TplName = "repo/home"
tplWatchers base.TplName = "repo/watchers" tplWatchers base.TplName = "repo/watchers"
tplForks base.TplName = "repo/forks" tplForks base.TplName = "repo/forks"
tplMigrating base.TplName = "repo/migrating" tplMigrating base.TplName = "repo/migrate/migrating"
) )
type namedBlob struct { type namedBlob struct {

@ -0,0 +1,103 @@
{{template "base/head" .}}
<div class="repository new migrate">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{.i18n.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{.i18n.Tr "repo.migrate.clone_address"}}</label>
<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}}
{{if .LFSActive}}<br/>{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
</span>
</div>
<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 class="inline field">
<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_options_mirror_disabled"}}</label>
{{else}}
<input id="mirror" name="mirror" type="checkbox" {{if .mirror}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_options_mirror_helper" | Safe}}</label>
{{end}}
</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>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">
{{.i18n.Tr "repo.migrate_repo"}}
</button>
<a class="ui button" href="{{AppSubUrl}}/">{{.i18n.Tr "cancel"}}</a>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

@ -5,7 +5,8 @@
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<h3 class="ui top attached header"> <h3 class="ui top attached header">
{{.i18n.Tr "new_migrate"}} {{.i18n.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
@ -18,31 +19,10 @@
</span> </span>
</div> </div>
<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">
{{range .Services}}
<div id="service-{{.}}" class="item" data-token="{{.TokenAuth}}" data-value="{{.}}">{{.Title}}</div>
{{end}}
</div>
</div>
</div>
<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 class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field {{if .Err_Auth}}error{{end}}">
<label for="auth_token">{{.i18n.Tr "access_token"}}</label> <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}}> <input id="auth_token" name="auth_token" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}>
<a target=”_blank” href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">{{svg "octicon-question" 16}}</a>
</div> </div>
<div class="inline field"> <div class="inline field">

@ -0,0 +1,137 @@
{{template "base/head" .}}
<div class="repository new migrate">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{.i18n.Tr "repo.migrate.migrate" .service.Title}}
<input id="service_type" type="hidden" name="service" value="{{.service}}">
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="inline required field {{if .Err_CloneAddr}}error{{end}}">
<label for="clone_addr">{{.i18n.Tr "repo.migrate.clone_address"}}</label>
<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}}
{{if .LFSActive}}<br/>{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
</span>
</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}}>
<a target=”_blank” href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">{{svg "octicon-question" 16}}</a>
</div>
<div class="inline field">
<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_options_mirror_disabled"}}</label>
{{else}}
<input id="mirror" name="mirror" type="checkbox" {{if .mirror}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_options_mirror_helper" | Safe}}</label>
{{end}}
</div>
</div>
<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">
<input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_wiki" | Safe}}</label>
</div>
<div class="ui checkbox">
<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_milestones" | Safe}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_labels" | Safe}}</label>
</div>
<div class="ui checkbox">
<input name="issues" type="checkbox" {{if .issues}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_issues" | Safe}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_merge_requests" | Safe}}</label>
</div>
<div class="ui checkbox">
<input name="releases" type="checkbox" {{if .releases}}checked{{end}}>
<label>{{.i18n.Tr "repo.migrate_items_releases" | Safe}}</label>
</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>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">
{{.i18n.Tr "repo.migrate_repo"}}
</button>
<a class="ui button" href="{{AppSubUrl}}/">{{.i18n.Tr "cancel"}}</a>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

@ -0,0 +1,23 @@
{{template "base/head" .}}
<div class="repository new migrate">
<div class="ui middle very relaxed page grid">
<div class="column">
<div class="ui three stackable cards">
{{range .Services}}
<div class="ui card">
<a class="image" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}">
{{svg (Printf "gitea-%s" .Name) 184}}
</a>
<div class="content">
<a class="header" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}">{{.Title}}</a>
<div class="description">
{{(Printf "repo.migrate.%s.description" .Name) | $.i18n.Tr }}
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

@ -25,14 +25,6 @@
<div v-show="tab === 'repos'" class="ui tab active list dashboard-repos"> <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.i18n.Tr "home.my_repos"}} <span class="ui grey label">${reposTotalCount}</span> {{.i18n.Tr "home.my_repos"}} <span class="ui grey label">${reposTotalCount}</span>
{{if or (not .ContextUser.IsOrganization) .IsOrganizationOwner}}
<div class="ui right">
<a class="poping up" :href="suburl + '/repo/create{{if .ContextUser.IsOrganization}}?org={{.ContextUser.ID}}{{end}}'" data-content="{{.i18n.Tr "new_repo"}}" data-variation="tiny inverted" data-position="left center">
<i class="plus icon"></i>
<span class="sr-only">{{.i18n.Tr "new_repo"}}</span>
</a>
</div>
{{end}}
</h4> </h4>
<div class="ui attached secondary segment repos-search"> <div class="ui attached secondary segment repos-search">
<div class="ui fluid right action left icon input" :class="{loading: isLoading}"> <div class="ui fluid right action left icon input" :class="{loading: isLoading}">

@ -7,7 +7,6 @@ const $items = $('#migrate_items').find('.field');
export default function initMigration() { export default function initMigration() {
checkAuth(); checkAuth();
$service.on('change', checkAuth);
$user.on('keyup', () => {checkItems(false)}); $user.on('keyup', () => {checkItems(false)});
$pass.on('keyup', () => {checkItems(false)}); $pass.on('keyup', () => {checkItems(false)});
$token.on('keyup', () => {checkItems(true)}); $token.on('keyup', () => {checkItems(true)});
@ -23,19 +22,8 @@ export default function initMigration() {
function checkAuth() { function checkAuth() {
const serviceType = $service.val(); const serviceType = $service.val();
const tokenAuth = $(`#service-${serviceType}`).data('token');
if (tokenAuth) { checkItems(serviceType !== 1);
$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) { function checkItems(tokenAuth) {

@ -3200,3 +3200,7 @@ td.blob-excerpt {
.select-project .item .svg { .select-project .item .svg {
margin-right: .5rem; margin-right: .5rem;
} }
.migrate .cards .card {
text-align: center;
}

@ -1994,3 +1994,19 @@ footer .container .links > * {
border: 1px solid rgba(121, 71, 66, .5) !important; border: 1px solid rgba(121, 71, 66, .5) !important;
border-bottom: none !important; border-bottom: none !important;
} }
.migrate .cards .card {
text-align: center;
}
.migrate .cards .card .content a {
color: rgb(158, 158, 158) !important;
}
.migrate .cards .card .content a:hover {
color: rgb(255, 255, 255) !important;
}
.migrate .cards .card .content .description {
color: rgb(158, 158, 158);
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="64px" height="64px"><path fill="#F4511E" d="M42.2,22.1L25.9,5.8C25.4,5.3,24.7,5,24,5c0,0,0,0,0,0c-0.7,0-1.4,0.3-1.9,0.8l-3.5,3.5l4.1,4.1c0.4-0.2,0.8-0.3,1.3-0.3c1.7,0,3,1.3,3,3c0,0.5-0.1,0.9-0.3,1.3l4,4c0.4-0.2,0.8-0.3,1.3-0.3c1.7,0,3,1.3,3,3s-1.3,3-3,3c-1.7,0-3-1.3-3-3c0-0.5,0.1-0.9,0.3-1.3l-4-4c-0.1,0-0.2,0.1-0.3,0.1v10.4c1.2,0.4,2,1.5,2,2.8c0,1.7-1.3,3-3,3s-3-1.3-3-3c0-1.3,0.8-2.4,2-2.8V18.8c-1.2-0.4-2-1.5-2-2.8c0-0.5,0.1-0.9,0.3-1.3l-4.1-4.1L5.8,22.1C5.3,22.6,5,23.3,5,24c0,0.7,0.3,1.4,0.8,1.9l16.3,16.3c0,0,0,0,0,0c0.5,0.5,1.2,0.8,1.9,0.8s1.4-0.3,1.9-0.8l16.3-16.3c0.5-0.5,0.8-1.2,0.8-1.9C43,23.3,42.7,22.6,42.2,22.1z"/></svg>

After

Width:  |  Height:  |  Size: 702 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64px" height="64px"><linearGradient id="KpzH_ttTMIjq8dhx1zD2pa" x1="30.999" x2="30.999" y1="16" y2="55.342" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#6dc7ff"/><stop offset="1" stop-color="#e6abff"/></linearGradient><path fill="url(#KpzH_ttTMIjq8dhx1zD2pa)" d="M25.008,56.007c-0.003-0.368-0.006-1.962-0.009-3.454l-0.003-1.55 c-6.729,0.915-8.358-3.78-8.376-3.83c-0.934-2.368-2.211-3.045-2.266-3.073l-0.124-0.072c-0.463-0.316-1.691-1.157-1.342-2.263 c0.315-0.997,1.536-1.1,2.091-1.082c3.074,0.215,4.63,2.978,4.694,3.095c1.569,2.689,3.964,2.411,5.509,1.844 c0.144-0.688,0.367-1.32,0.659-1.878C20.885,42.865,15.27,40.229,15.27,30.64c0-2.633,0.82-4.96,2.441-6.929 c-0.362-1.206-0.774-3.666,0.446-6.765l0.174-0.442l0.452-0.144c0.416-0.137,2.688-0.624,7.359,2.433 c1.928-0.494,3.969-0.749,6.074-0.759c2.115,0.01,4.158,0.265,6.09,0.759c4.667-3.058,6.934-2.565,7.351-2.433l0.451,0.145 l0.174,0.44c1.225,3.098,0.813,5.559,0.451,6.766c1.618,1.963,2.438,4.291,2.438,6.929c0,9.591-5.621,12.219-10.588,13.087 c0.563,1.065,0.868,2.402,0.868,3.878c0,1.683-0.007,7.204-0.015,8.402l-2-0.014c0.008-1.196,0.015-6.708,0.015-8.389 c0-2.442-0.943-3.522-1.35-3.874l-1.73-1.497l2.274-0.253c5.205-0.578,10.525-2.379,10.525-11.341c0-2.33-0.777-4.361-2.31-6.036 l-0.43-0.469l0.242-0.587c0.166-0.401,0.894-2.442-0.043-5.291c-0.758,0.045-2.568,0.402-5.584,2.447l-0.384,0.259l-0.445-0.123 c-1.863-0.518-3.938-0.796-6.001-0.806c-2.052,0.01-4.124,0.288-5.984,0.806l-0.445,0.123l-0.383-0.259 c-3.019-2.044-4.833-2.404-5.594-2.449c-0.935,2.851-0.206,4.892-0.04,5.293l0.242,0.587l-0.429,0.469 c-1.536,1.681-2.314,3.712-2.314,6.036c0,8.958,5.31,10.77,10.504,11.361l2.252,0.256l-1.708,1.49 c-0.372,0.325-1.03,1.112-1.254,2.727l-0.075,0.549l-0.506,0.227c-1.321,0.592-5.839,2.162-8.548-2.485 c-0.015-0.025-0.544-0.945-1.502-1.557c0.646,0.639,1.433,1.673,2.068,3.287c0.066,0.19,1.357,3.622,7.28,2.339l1.206-0.262 l0.012,3.978c0.003,1.487,0.006,3.076,0.009,3.444L25.008,56.007z"/><linearGradient id="KpzH_ttTMIjq8dhx1zD2pb" x1="32" x2="32" y1="5" y2="59.167" gradientUnits="userSpaceOnUse" spreadMethod="reflect"><stop offset="0" stop-color="#1a6dff"/><stop offset="1" stop-color="#c822ff"/></linearGradient><path fill="url(#KpzH_ttTMIjq8dhx1zD2pb)" d="M32,58C17.663,58,6,46.337,6,32S17.663,6,32,6s26,11.663,26,26S46.337,58,32,58z M32,8 C18.767,8,8,18.767,8,32s10.767,24,24,24s24-10.767,24-24S45.233,8,32,8z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="64px" height="64px"><path fill="#e53935" d="M24 43L16 20 32 20z"/><path fill="#ff7043" d="M24 43L42 20 32 20z"/><path fill="#e53935" d="M37 5L42 20 32 20z"/><path fill="#ffa726" d="M24 43L42 20 45 28z"/><path fill="#ff7043" d="M24 43L6 20 16 20z"/><path fill="#e53935" d="M11 5L6 20 16 20z"/><path fill="#ffa726" d="M24 43L6 20 3 28z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

Loading…
Cancel
Save