* mv migrate * mv mirror Co-authored-by: zeripath <art27@cantab.net>tags/v1.10.5
@@ -0,0 +1,216 @@ | |||
// 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 ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
"net/http" | |||
"net/url" | |||
"strings" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/auth" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/graceful" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/migrations" | |||
"code.gitea.io/gitea/modules/notification" | |||
repo_module "code.gitea.io/gitea/modules/repository" | |||
"code.gitea.io/gitea/modules/setting" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
// 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" | |||
// "403": | |||
// "$ref": "#/responses/forbidden" | |||
// "422": | |||
// "$ref": "#/responses/validationError" | |||
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(http.StatusUnprocessableEntity, "", err) | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "GetUserByID", err) | |||
} | |||
return | |||
} | |||
ctxUser = org | |||
} | |||
if ctx.HasError() { | |||
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) | |||
return | |||
} | |||
if !ctx.User.IsAdmin { | |||
if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID { | |||
ctx.Error(http.StatusForbidden, "", "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(http.StatusInternalServerError, "IsOwnedBy", err) | |||
return | |||
} else if !isOwner { | |||
ctx.Error(http.StatusForbidden, "", "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(http.StatusUnprocessableEntity, "", err) | |||
case addrErr.IsPermissionDenied: | |||
ctx.Error(http.StatusUnprocessableEntity, "", "You are not allowed to import local repositories.") | |||
case addrErr.IsInvalidPath: | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.") | |||
default: | |||
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error()) | |||
} | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", err) | |||
} | |||
return | |||
} | |||
var gitServiceType = api.PlainGitService | |||
u, err := url.Parse(remoteAddr) | |||
if err == nil && strings.EqualFold(u.Host, "github.com") { | |||
gitServiceType = api.GithubService | |||
} | |||
var opts = migrations.MigrateOptions{ | |||
CloneAddr: remoteAddr, | |||
RepoName: form.RepoName, | |||
Description: form.Description, | |||
Private: form.Private || setting.Repository.ForcePrivate, | |||
Mirror: form.Mirror, | |||
AuthUsername: form.AuthUsername, | |||
AuthPassword: form.AuthPassword, | |||
Wiki: form.Wiki, | |||
Issues: form.Issues, | |||
Milestones: form.Milestones, | |||
Labels: form.Labels, | |||
Comments: true, | |||
PullRequests: form.PullRequests, | |||
Releases: form.Releases, | |||
GitServiceType: gitServiceType, | |||
} | |||
if opts.Mirror { | |||
opts.Issues = false | |||
opts.Milestones = false | |||
opts.Labels = false | |||
opts.Comments = false | |||
opts.PullRequests = false | |||
opts.Releases = false | |||
} | |||
repo, err := repo_module.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ | |||
Name: opts.RepoName, | |||
Description: opts.Description, | |||
OriginalURL: form.CloneAddr, | |||
GitServiceType: gitServiceType, | |||
IsPrivate: opts.Private, | |||
IsMirror: opts.Mirror, | |||
Status: models.RepositoryBeingMigrated, | |||
}) | |||
if err != nil { | |||
handleMigrateError(ctx, ctxUser, remoteAddr, err) | |||
return | |||
} | |||
opts.MigrateToRepoID = repo.ID | |||
defer func() { | |||
if e := recover(); e != nil { | |||
var buf bytes.Buffer | |||
fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) | |||
err = errors.New(buf.String()) | |||
} | |||
if err == nil { | |||
repo.Status = models.RepositoryReady | |||
if err := models.UpdateRepositoryCols(repo, "status"); err == nil { | |||
notification.NotifyMigrateRepository(ctx.User, ctxUser, repo) | |||
return | |||
} | |||
} | |||
if repo != nil { | |||
if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil { | |||
log.Error("DeleteRepository: %v", errDelete) | |||
} | |||
} | |||
}() | |||
if _, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, ctxUser.Name, opts); err != nil { | |||
handleMigrateError(ctx, ctxUser, remoteAddr, err) | |||
return | |||
} | |||
log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName) | |||
ctx.JSON(http.StatusCreated, repo.APIFormat(models.AccessModeAdmin)) | |||
} | |||
func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteAddr string, err error) { | |||
switch { | |||
case models.IsErrRepoAlreadyExist(err): | |||
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") | |||
case migrations.IsRateLimitError(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") | |||
case migrations.IsTwoFactorAuthError(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit required two factors authentication.") | |||
case models.IsErrReachLimitOfRepo(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) | |||
case models.IsErrNameReserved(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)) | |||
case models.IsErrNamePatternNotAllowed(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)) | |||
default: | |||
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.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Authentication failed: %v.", err)) | |||
} else if strings.Contains(err.Error(), "fatal:") { | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Migration failed: %v.", err)) | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "MigrateRepository", err) | |||
} | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
// 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 ( | |||
"net/http" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/context" | |||
mirror_service "code.gitea.io/gitea/services/mirror" | |||
) | |||
// MirrorSync adds a mirrored repository to the sync queue | |||
func MirrorSync(ctx *context.APIContext) { | |||
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync | |||
// --- | |||
// summary: Sync a mirrored repository | |||
// produces: | |||
// - application/json | |||
// parameters: | |||
// - name: owner | |||
// in: path | |||
// description: owner of the repo to sync | |||
// type: string | |||
// required: true | |||
// - name: repo | |||
// in: path | |||
// description: name of the repo to sync | |||
// type: string | |||
// required: true | |||
// responses: | |||
// "200": | |||
// "$ref": "#/responses/empty" | |||
// "403": | |||
// "$ref": "#/responses/forbidden" | |||
repo := ctx.Repo.Repository | |||
if !ctx.Repo.CanWrite(models.UnitTypeCode) { | |||
ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access") | |||
} | |||
mirror_service.StartToMirror(repo.ID) | |||
ctx.Status(http.StatusOK) | |||
} |
@@ -6,28 +6,19 @@ | |||
package repo | |||
import ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
"net/http" | |||
"net/url" | |||
"strings" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/auth" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/graceful" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/migrations" | |||
"code.gitea.io/gitea/modules/notification" | |||
repo_module "code.gitea.io/gitea/modules/repository" | |||
"code.gitea.io/gitea/modules/setting" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/modules/validation" | |||
"code.gitea.io/gitea/routers/api/v1/utils" | |||
mirror_service "code.gitea.io/gitea/services/mirror" | |||
repo_service "code.gitea.io/gitea/services/repository" | |||
) | |||
@@ -367,196 +358,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" | |||
// "403": | |||
// "$ref": "#/responses/forbidden" | |||
// "422": | |||
// "$ref": "#/responses/validationError" | |||
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(http.StatusUnprocessableEntity, "", err) | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "GetUserByID", err) | |||
} | |||
return | |||
} | |||
ctxUser = org | |||
} | |||
if ctx.HasError() { | |||
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) | |||
return | |||
} | |||
if !ctx.User.IsAdmin { | |||
if !ctxUser.IsOrganization() && ctx.User.ID != ctxUser.ID { | |||
ctx.Error(http.StatusForbidden, "", "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(http.StatusInternalServerError, "IsOwnedBy", err) | |||
return | |||
} else if !isOwner { | |||
ctx.Error(http.StatusForbidden, "", "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(http.StatusUnprocessableEntity, "", err) | |||
case addrErr.IsPermissionDenied: | |||
ctx.Error(http.StatusUnprocessableEntity, "", "You are not allowed to import local repositories.") | |||
case addrErr.IsInvalidPath: | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.") | |||
default: | |||
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error()) | |||
} | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", err) | |||
} | |||
return | |||
} | |||
var gitServiceType = api.PlainGitService | |||
u, err := url.Parse(remoteAddr) | |||
if err == nil && strings.EqualFold(u.Host, "github.com") { | |||
gitServiceType = api.GithubService | |||
} | |||
var opts = migrations.MigrateOptions{ | |||
CloneAddr: remoteAddr, | |||
RepoName: form.RepoName, | |||
Description: form.Description, | |||
Private: form.Private || setting.Repository.ForcePrivate, | |||
Mirror: form.Mirror, | |||
AuthUsername: form.AuthUsername, | |||
AuthPassword: form.AuthPassword, | |||
Wiki: form.Wiki, | |||
Issues: form.Issues, | |||
Milestones: form.Milestones, | |||
Labels: form.Labels, | |||
Comments: true, | |||
PullRequests: form.PullRequests, | |||
Releases: form.Releases, | |||
GitServiceType: gitServiceType, | |||
} | |||
if opts.Mirror { | |||
opts.Issues = false | |||
opts.Milestones = false | |||
opts.Labels = false | |||
opts.Comments = false | |||
opts.PullRequests = false | |||
opts.Releases = false | |||
} | |||
repo, err := repo_module.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ | |||
Name: opts.RepoName, | |||
Description: opts.Description, | |||
OriginalURL: form.CloneAddr, | |||
GitServiceType: gitServiceType, | |||
IsPrivate: opts.Private, | |||
IsMirror: opts.Mirror, | |||
Status: models.RepositoryBeingMigrated, | |||
}) | |||
if err != nil { | |||
handleMigrateError(ctx, ctxUser, remoteAddr, err) | |||
return | |||
} | |||
opts.MigrateToRepoID = repo.ID | |||
defer func() { | |||
if e := recover(); e != nil { | |||
var buf bytes.Buffer | |||
fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) | |||
err = errors.New(buf.String()) | |||
} | |||
if err == nil { | |||
repo.Status = models.RepositoryReady | |||
if err := models.UpdateRepositoryCols(repo, "status"); err == nil { | |||
notification.NotifyMigrateRepository(ctx.User, ctxUser, repo) | |||
return | |||
} | |||
} | |||
if repo != nil { | |||
if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil { | |||
log.Error("DeleteRepository: %v", errDelete) | |||
} | |||
} | |||
}() | |||
if _, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, ctxUser.Name, opts); err != nil { | |||
handleMigrateError(ctx, ctxUser, remoteAddr, err) | |||
return | |||
} | |||
log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName) | |||
ctx.JSON(http.StatusCreated, repo.APIFormat(models.AccessModeAdmin)) | |||
} | |||
func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteAddr string, err error) { | |||
switch { | |||
case models.IsErrRepoAlreadyExist(err): | |||
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") | |||
case migrations.IsRateLimitError(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") | |||
case migrations.IsTwoFactorAuthError(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit required two factors authentication.") | |||
case models.IsErrReachLimitOfRepo(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) | |||
case models.IsErrNameReserved(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)) | |||
case models.IsErrNamePatternNotAllowed(err): | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)) | |||
default: | |||
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.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Authentication failed: %v.", err)) | |||
} else if strings.Contains(err.Error(), "fatal:") { | |||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Migration failed: %v.", err)) | |||
} else { | |||
ctx.Error(http.StatusInternalServerError, "MigrateRepository", err) | |||
} | |||
} | |||
} | |||
// Get one repository | |||
func Get(ctx *context.APIContext) { | |||
// swagger:operation GET /repos/{owner}/{repo} repository repoGet | |||
@@ -982,38 +783,3 @@ func Delete(ctx *context.APIContext) { | |||
log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) | |||
ctx.Status(http.StatusNoContent) | |||
} | |||
// MirrorSync adds a mirrored repository to the sync queue | |||
func MirrorSync(ctx *context.APIContext) { | |||
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync | |||
// --- | |||
// summary: Sync a mirrored repository | |||
// produces: | |||
// - application/json | |||
// parameters: | |||
// - name: owner | |||
// in: path | |||
// description: owner of the repo to sync | |||
// type: string | |||
// required: true | |||
// - name: repo | |||
// in: path | |||
// description: name of the repo to sync | |||
// type: string | |||
// required: true | |||
// responses: | |||
// "200": | |||
// "$ref": "#/responses/empty" | |||
// "403": | |||
// "$ref": "#/responses/forbidden" | |||
repo := ctx.Repo.Repository | |||
if !ctx.Repo.CanWrite(models.UnitTypeCode) { | |||
ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access") | |||
} | |||
mirror_service.StartToMirror(repo.ID) | |||
ctx.Status(http.StatusOK) | |||
} |