forked from gitdab/gitdab
Add changes for disabled migrations
This commit is contained in:
parent
d40b274f34
commit
52b62b36b3
3 changed files with 392 additions and 0 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
Gitdab's scripts, icons and gitea customizations
|
Gitdab's scripts, icons and gitea customizations
|
||||||
|
|
||||||
|
We run a fork of v1.6 with following changes:
|
||||||
|
- Repo migrations are disabled to prevent leaking server's IP address.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- Luna: Idea for Gitdab
|
- Luna: Idea for Gitdab
|
||||||
|
|
132
customizations/custom/templates/base/head_navbar.tmpl
Normal file
132
customizations/custom/templates/base/head_navbar.tmpl
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<div class="ui container" id="navbar">
|
||||||
|
<div class="item brand" style="justify-content: space-between;">
|
||||||
|
<a href="{{AppSubUrl}}/">
|
||||||
|
<img class="ui mini image" src="{{AppSubUrl}}/img/gitea-sm.png">
|
||||||
|
</a>
|
||||||
|
<div class="ui basic icon button mobile-only" id="navbar-expand-toggle">
|
||||||
|
<i class="sidebar icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .IsSigned}}
|
||||||
|
<a class="item {{if .PageIsDashboard}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "dashboard"}}</a>
|
||||||
|
<a class="item {{if .PageIsIssues}}active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a>
|
||||||
|
<a class="item {{if .PageIsPulls}}active{{end}}" href="{{AppSubUrl}}/pulls">{{.i18n.Tr "pull_requests"}}</a>
|
||||||
|
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
|
||||||
|
{{else if .IsLandingPageHome}}
|
||||||
|
<a class="item {{if .PageIsHome}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "home"}}</a>
|
||||||
|
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
|
||||||
|
{{else if .IsLandingPageExplore}}
|
||||||
|
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "home"}}</a>
|
||||||
|
{{else if .IsLandingPageOrganizations}}
|
||||||
|
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/organizations">{{.i18n.Tr "home"}}</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "custom/extra_links" .}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
<div class="item">
|
||||||
|
<div class="ui icon input">
|
||||||
|
<input class="searchbox" type="text" placeholder="{{.i18n.Tr "search_project"}}">
|
||||||
|
<i class="search icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{if .IsSigned}}
|
||||||
|
<div class="right stackable menu">
|
||||||
|
<a href="{{AppSubUrl}}/notifications" class="item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted">
|
||||||
|
<span class="text">
|
||||||
|
<i class="fitted octicon octicon-bell"></i>
|
||||||
|
<span class="sr-mobile-only">{{.i18n.Tr "notifications"}}</span>
|
||||||
|
|
||||||
|
{{if .NotificationUnreadCount}}
|
||||||
|
<span class="ui red label">
|
||||||
|
{{.NotificationUnreadCount}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="ui dropdown jump item poping up" data-content="{{.i18n.Tr "create_new"}}" data-variation="tiny inverted">
|
||||||
|
<span class="text">
|
||||||
|
<i class="fitted octicon octicon-plus"></i>
|
||||||
|
<span class="sr-mobile-only">{{.i18n.Tr "create_new"}}</span>
|
||||||
|
<i class="fitted octicon octicon-triangle-down not-mobile"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item" href="{{AppSubUrl}}/repo/create">
|
||||||
|
<i class="fitted octicon octicon-plus"></i> {{.i18n.Tr "new_repo"}}
|
||||||
|
</a>
|
||||||
|
{{if .SignedUser.CanCreateOrganization}}
|
||||||
|
<a class="item" href="{{AppSubUrl}}/org/create">
|
||||||
|
<i class="fitted octicon octicon-organization"></i> {{.i18n.Tr "new_org"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div><!-- end content create new menu -->
|
||||||
|
</div><!-- end dropdown menu create new -->
|
||||||
|
|
||||||
|
<div class="ui dropdown jump item poping up" tabindex="-1" data-content="{{.i18n.Tr "user_profile_and_more"}}" data-variation="tiny inverted">
|
||||||
|
<span class="text">
|
||||||
|
<img class="ui tiny avatar image" src="{{.SignedUser.RelAvatarLink}}">
|
||||||
|
<span class="sr-only">{{.i18n.Tr "user_profile_and_more"}}</span>
|
||||||
|
<span class="mobile-only">{{.SignedUser.Name}}</span>
|
||||||
|
<i class="fitted octicon octicon-triangle-down not-mobile" tabindex="-1"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu user-menu" tabindex="-1">
|
||||||
|
<div class="ui header">
|
||||||
|
{{.i18n.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<a class="item" href="{{AppSubUrl}}/{{.SignedUser.Name}}">
|
||||||
|
<i class="octicon octicon-person"></i>
|
||||||
|
{{.i18n.Tr "your_profile"}}<!-- Your profile -->
|
||||||
|
</a>
|
||||||
|
<a class="item" href="{{AppSubUrl}}/{{.SignedUser.Name}}?tab=stars">
|
||||||
|
<i class="octicon octicon-star"></i>
|
||||||
|
{{.i18n.Tr "your_starred"}}
|
||||||
|
</a>
|
||||||
|
<a class="{{if .PageIsUserSettings}}active{{end}} item" href="{{AppSubUrl}}/user/settings">
|
||||||
|
<i class="octicon octicon-settings"></i>
|
||||||
|
{{.i18n.Tr "your_settings"}}<!-- Your settings -->
|
||||||
|
</a>
|
||||||
|
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io">
|
||||||
|
<i class="octicon octicon-question"></i>
|
||||||
|
{{.i18n.Tr "help"}}<!-- Help -->
|
||||||
|
</a>
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<a class="{{if .PageIsAdmin}}active{{end}} item" href="{{AppSubUrl}}/admin">
|
||||||
|
<i class="icon settings"></i>
|
||||||
|
{{.i18n.Tr "admin_panel"}}<!-- Admin Panel -->
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<a class="item" href="{{AppSubUrl}}/user/logout">
|
||||||
|
<i class="octicon octicon-sign-out"></i>
|
||||||
|
{{.i18n.Tr "sign_out"}}<!-- Sign Out -->
|
||||||
|
</a>
|
||||||
|
</div><!-- end content avatar menu -->
|
||||||
|
</div><!-- end dropdown avatar menu -->
|
||||||
|
</div><!-- end signed user right menu -->
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
|
||||||
|
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io">{{.i18n.Tr "help"}}</a>
|
||||||
|
<div class="right stackable menu">
|
||||||
|
{{if .ShowRegistrationButton}}
|
||||||
|
<a class="item{{if .PageIsSignUp}} active{{end}}" href="{{AppSubUrl}}/user/sign_up">
|
||||||
|
<i class="octicon octicon-person"></i> {{.i18n.Tr "register"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="item{{if .PageIsSignIn}} active{{end}}" href="{{AppSubUrl}}/user/login?redirect_to={{.Link}}">
|
||||||
|
<i class="octicon octicon-sign-in"></i> {{.i18n.Tr "sign_in"}}
|
||||||
|
</a>
|
||||||
|
</div><!-- end anonymous right menu -->
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
257
customizations/giteadiff.diff
Normal file
257
customizations/giteadiff.diff
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
|
||||||
|
index fe54aa2a3..6fba223ad 100644
|
||||||
|
--- a/routers/api/v1/api.go
|
||||||
|
+++ b/routers/api/v1/api.go
|
||||||
|
@@ -431,8 +431,6 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
|
m.Combo("/repositories/:id", reqToken()).Get(repo.GetByID)
|
||||||
|
|
||||||
|
m.Group("/repos", func() {
|
||||||
|
- m.Post("/migrate", reqToken(), bind(auth.MigrateRepoForm{}), repo.Migrate)
|
||||||
|
-
|
||||||
|
m.Group("/:username/:reponame", func() {
|
||||||
|
m.Combo("").Get(repo.Get).Delete(reqToken(), repo.Delete)
|
||||||
|
m.Group("/hooks", func() {
|
||||||
|
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
|
||||||
|
index e48b100af..d8501a552 100644
|
||||||
|
--- a/routers/api/v1/repo/repo.go
|
||||||
|
+++ b/routers/api/v1/repo/repo.go
|
||||||
|
@@ -10,7 +10,6 @@ import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
- "code.gitea.io/gitea/modules/auth"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@@ -318,105 +317,6 @@ func CreateOrgRepo(ctx *context.APIContext, opt api.CreateRepoOption) {
|
||||||
|
CreateUserRepo(ctx, org, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
-// Migrate migrate remote git repository to gitea
|
||||||
|
-func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
|
||||||
|
- // swagger:operation POST /repos/migrate repository repoMigrate
|
||||||
|
- // ---
|
||||||
|
- // summary: Migrate a remote git repository
|
||||||
|
- // consumes:
|
||||||
|
- // - application/json
|
||||||
|
- // produces:
|
||||||
|
- // - application/json
|
||||||
|
- // parameters:
|
||||||
|
- // - name: body
|
||||||
|
- // in: body
|
||||||
|
- // schema:
|
||||||
|
- // "$ref": "#/definitions/MigrateRepoForm"
|
||||||
|
- // responses:
|
||||||
|
- // "201":
|
||||||
|
- // "$ref": "#/responses/Repository"
|
||||||
|
- ctxUser := ctx.User
|
||||||
|
- // Not equal means context user is an organization,
|
||||||
|
- // or is another user/organization if current user is admin.
|
||||||
|
- if form.UID != ctxUser.ID {
|
||||||
|
- org, err := models.GetUserByID(form.UID)
|
||||||
|
- if err != nil {
|
||||||
|
- if models.IsErrUserNotExist(err) {
|
||||||
|
- ctx.Error(422, "", err)
|
||||||
|
- } else {
|
||||||
|
- ctx.Error(500, "GetUserByID", err)
|
||||||
|
- }
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
- ctxUser = org
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if ctx.HasError() {
|
||||||
|
- ctx.Error(422, "", ctx.GetErrMsg())
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if !ctx.User.IsAdmin {
|
||||||
|
- if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID {
|
||||||
|
- ctx.Error(403, "", "Given user is not an organization.")
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if ctxUser.IsOrganization() {
|
||||||
|
- // Check ownership of organization.
|
||||||
|
- isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
|
||||||
|
- if err != nil {
|
||||||
|
- ctx.Error(500, "IsOwnedBy", err)
|
||||||
|
- return
|
||||||
|
- } else if !isOwner {
|
||||||
|
- ctx.Error(403, "", "Given user is not owner of organization.")
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- remoteAddr, err := form.ParseRemoteAddr(ctx.User)
|
||||||
|
- if err != nil {
|
||||||
|
- if models.IsErrInvalidCloneAddr(err) {
|
||||||
|
- addrErr := err.(models.ErrInvalidCloneAddr)
|
||||||
|
- switch {
|
||||||
|
- case addrErr.IsURLError:
|
||||||
|
- ctx.Error(422, "", err)
|
||||||
|
- case addrErr.IsPermissionDenied:
|
||||||
|
- ctx.Error(422, "", "You are not allowed to import local repositories.")
|
||||||
|
- case addrErr.IsInvalidPath:
|
||||||
|
- ctx.Error(422, "", "Invalid local path, it does not exist or not a directory.")
|
||||||
|
- default:
|
||||||
|
- ctx.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
|
||||||
|
- }
|
||||||
|
- } else {
|
||||||
|
- ctx.Error(500, "ParseRemoteAddr", err)
|
||||||
|
- }
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{
|
||||||
|
- Name: form.RepoName,
|
||||||
|
- Description: form.Description,
|
||||||
|
- IsPrivate: form.Private || setting.Repository.ForcePrivate,
|
||||||
|
- IsMirror: form.Mirror,
|
||||||
|
- RemoteAddr: remoteAddr,
|
||||||
|
- })
|
||||||
|
- if err != nil {
|
||||||
|
- err = util.URLSanitizedError(err, remoteAddr)
|
||||||
|
- if repo != nil {
|
||||||
|
- if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
|
||||||
|
- log.Error(4, "DeleteRepository: %v", errDelete)
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
- ctx.Error(500, "MigrateRepository", err)
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName)
|
||||||
|
- ctx.JSON(201, repo.APIFormat(models.AccessModeAdmin))
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
// Get one repository
|
||||||
|
func Get(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo} repository repoGet
|
||||||
|
diff --git a/routers/repo/repo.go b/routers/repo/repo.go
|
||||||
|
index 236d66bd1..aa6cfe401 100644
|
||||||
|
--- a/routers/repo/repo.go
|
||||||
|
+++ b/routers/repo/repo.go
|
||||||
|
@@ -20,12 +20,10 @@ import (
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
- "code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplCreate base.TplName = "repo/create"
|
||||||
|
- tplMigrate base.TplName = "repo/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustBeNotBare render when a repo is a bare git dir
|
||||||
|
@@ -190,95 +188,6 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) {
|
||||||
|
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["mirror"] = ctx.Query("mirror") == "1"
|
||||||
|
- ctx.Data["LFSActive"] = setting.LFS.StartServer
|
||||||
|
-
|
||||||
|
- ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
|
||||||
|
- if ctx.Written() {
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
- ctx.Data["ContextUser"] = ctxUser
|
||||||
|
-
|
||||||
|
- ctx.HTML(200, tplMigrate)
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
-// MigratePost response for migrating from external git repository
|
||||||
|
-func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) {
|
||||||
|
- ctx.Data["Title"] = ctx.Tr("new_migrate")
|
||||||
|
-
|
||||||
|
- 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
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{
|
||||||
|
- Name: form.RepoName,
|
||||||
|
- Description: form.Description,
|
||||||
|
- IsPrivate: form.Private || setting.Repository.ForcePrivate,
|
||||||
|
- IsMirror: form.Mirror,
|
||||||
|
- RemoteAddr: remoteAddr,
|
||||||
|
- })
|
||||||
|
- if err == nil {
|
||||||
|
- log.Trace("Repository migrated [%d]: %s/%s", repo.ID, ctxUser.Name, form.RepoName)
|
||||||
|
- ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + form.RepoName)
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- // remoteAddr may contain credentials, so we sanitize it
|
||||||
|
- err = util.URLSanitizedError(err, remoteAddr)
|
||||||
|
-
|
||||||
|
- if repo != nil {
|
||||||
|
- if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
|
||||||
|
- log.Error(4, "DeleteRepository: %v", errDelete)
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if strings.Contains(err.Error(), "Authentication failed") ||
|
||||||
|
- strings.Contains(err.Error(), "could not read Username") {
|
||||||
|
- ctx.Data["Err_Auth"] = true
|
||||||
|
- ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tplMigrate, &form)
|
||||||
|
- return
|
||||||
|
- } else if strings.Contains(err.Error(), "fatal:") {
|
||||||
|
- ctx.Data["Err_CloneAddr"] = true
|
||||||
|
- ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tplMigrate, &form)
|
||||||
|
- return
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- handleCreateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form)
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
// Action response for actions to a repository
|
||||||
|
func Action(ctx *context.Context) {
|
||||||
|
var err error
|
||||||
|
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
|
||||||
|
index 4ca421065..859e54981 100644
|
||||||
|
--- a/routers/routes/routes.go
|
||||||
|
+++ b/routers/routes/routes.go
|
||||||
|
@@ -455,8 +455,6 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
|
m.Group("/repo", func() {
|
||||||
|
m.Get("/create", repo.Create)
|
||||||
|
m.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost)
|
||||||
|
- m.Get("/migrate", repo.Migrate)
|
||||||
|
- m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost)
|
||||||
|
m.Group("/fork", func() {
|
||||||
|
m.Combo("/:repoid").Get(repo.Fork).
|
||||||
|
Post(bindIgnErr(auth.CreateRepoForm{}), repo.ForkPost)
|
Loading…
Reference in a new issue