2020-01-29 06:55:23 +01:00
// 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"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
2020-09-11 00:29:19 +02:00
"code.gitea.io/gitea/modules/convert"
2020-01-29 06:55:23 +01:00
"code.gitea.io/gitea/modules/graceful"
2021-04-09 00:25:57 +02:00
"code.gitea.io/gitea/modules/lfs"
2020-01-29 06:55:23 +01:00
"code.gitea.io/gitea/modules/log"
2021-11-16 16:25:33 +01:00
base "code.gitea.io/gitea/modules/migration"
2020-01-29 06:55:23 +01:00
"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"
2021-01-26 16:36:53 +01:00
"code.gitea.io/gitea/modules/web"
2021-04-06 21:44:05 +02:00
"code.gitea.io/gitea/services/forms"
2021-11-16 16:25:33 +01:00
"code.gitea.io/gitea/services/migrations"
2020-01-29 06:55:23 +01:00
)
// Migrate migrate remote git repository to gitea
2021-01-26 16:36:53 +01:00
func Migrate ( ctx * context . APIContext ) {
2020-01-29 06:55:23 +01:00
// 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:
2020-09-11 00:29:19 +02:00
// "$ref": "#/definitions/MigrateRepoOptions"
2020-01-29 06:55:23 +01:00
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 16:36:53 +01:00
form := web . GetForm ( ctx ) . ( * api . MigrateRepoOptions )
2020-09-11 00:29:19 +02:00
//get repoOwner
var (
repoOwner * models . User
err error
)
if len ( form . RepoOwner ) != 0 {
repoOwner , err = models . GetUserByName ( form . RepoOwner )
} else if form . RepoOwnerID != 0 {
repoOwner , err = models . GetUserByID ( form . RepoOwnerID )
} else {
repoOwner = ctx . User
}
if err != nil {
if models . IsErrUserNotExist ( err ) {
ctx . Error ( http . StatusUnprocessableEntity , "" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetUser" , err )
2020-01-29 06:55:23 +01:00
}
2020-09-11 00:29:19 +02:00
return
2020-01-29 06:55:23 +01:00
}
if ctx . HasError ( ) {
ctx . Error ( http . StatusUnprocessableEntity , "" , ctx . GetErrMsg ( ) )
return
}
if ! ctx . User . IsAdmin {
2020-09-11 00:29:19 +02:00
if ! repoOwner . IsOrganization ( ) && ctx . User . ID != repoOwner . ID {
2020-01-29 06:55:23 +01:00
ctx . Error ( http . StatusForbidden , "" , "Given user is not an organization." )
return
}
2020-09-11 00:29:19 +02:00
if repoOwner . IsOrganization ( ) {
2020-01-29 06:55:23 +01:00
// Check ownership of organization.
2021-11-19 12:41:40 +01:00
isOwner , err := models . OrgFromUser ( repoOwner ) . IsOwnedBy ( ctx . User . ID )
2020-01-29 06:55:23 +01:00
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
}
}
}
2021-04-06 21:44:05 +02:00
remoteAddr , err := forms . ParseRemoteAddr ( form . CloneAddr , form . AuthUsername , form . AuthPassword )
2021-03-15 22:52:11 +01:00
if err == nil {
err = migrations . IsMigrateURLAllowed ( remoteAddr , ctx . User )
}
2020-01-29 06:55:23 +01:00
if err != nil {
2021-04-09 00:25:57 +02:00
handleRemoteAddrError ( ctx , err )
2020-01-29 06:55:23 +01:00
return
}
2020-09-11 00:29:19 +02:00
gitServiceType := convert . ToGitServiceType ( form . Service )
2020-01-29 06:55:23 +01:00
2021-09-07 17:49:36 +02:00
if form . Mirror && setting . Mirror . DisableNewPull {
ctx . Error ( http . StatusForbidden , "MirrorsGlobalDisabled" , fmt . Errorf ( "the site administrator has disabled the creation of new pull mirrors" ) )
2020-06-04 20:06:24 +02:00
return
2020-06-04 18:11:28 +02:00
}
2020-12-21 15:39:41 +01:00
if setting . Repository . DisableMigrations {
ctx . Error ( http . StatusForbidden , "MigrationsGlobalDisabled" , fmt . Errorf ( "the site administrator has disabled migrations" ) )
return
}
2021-04-09 00:25:57 +02:00
form . LFS = form . LFS && setting . LFS . StartServer
if form . LFS && len ( form . LFSEndpoint ) > 0 {
ep := lfs . DetermineEndpoint ( "" , form . LFSEndpoint )
if ep == nil {
ctx . Error ( http . StatusInternalServerError , "" , ctx . Tr ( "repo.migrate.invalid_lfs_endpoint" ) )
return
}
err = migrations . IsMigrateURLAllowed ( ep . String ( ) , ctx . User )
if err != nil {
handleRemoteAddrError ( ctx , err )
return
}
}
2020-01-29 06:55:23 +01:00
var opts = migrations . MigrateOptions {
CloneAddr : remoteAddr ,
RepoName : form . RepoName ,
Description : form . Description ,
Private : form . Private || setting . Repository . ForcePrivate ,
2020-06-04 18:11:28 +02:00
Mirror : form . Mirror ,
2021-04-09 00:25:57 +02:00
LFS : form . LFS ,
LFSEndpoint : form . LFSEndpoint ,
2020-01-29 06:55:23 +01:00
AuthUsername : form . AuthUsername ,
AuthPassword : form . AuthPassword ,
2020-09-11 00:29:19 +02:00
AuthToken : form . AuthToken ,
2020-01-29 06:55:23 +01:00
Wiki : form . Wiki ,
Issues : form . Issues ,
Milestones : form . Milestones ,
Labels : form . Labels ,
Comments : true ,
PullRequests : form . PullRequests ,
Releases : form . Releases ,
GitServiceType : gitServiceType ,
2021-01-03 00:47:47 +01:00
MirrorInterval : form . MirrorInterval ,
2020-01-29 06:55:23 +01:00
}
if opts . Mirror {
opts . Issues = false
opts . Milestones = false
opts . Labels = false
opts . Comments = false
opts . PullRequests = false
opts . Releases = false
}
2020-09-11 00:29:19 +02:00
repo , err := repo_module . CreateRepository ( ctx . User , repoOwner , models . CreateRepoOptions {
2020-01-29 06:55:23 +01:00
Name : opts . RepoName ,
Description : opts . Description ,
OriginalURL : form . CloneAddr ,
GitServiceType : gitServiceType ,
IsPrivate : opts . Private ,
IsMirror : opts . Mirror ,
Status : models . RepositoryBeingMigrated ,
} )
if err != nil {
2020-09-11 00:29:19 +02:00
handleMigrateError ( ctx , repoOwner , remoteAddr , err )
2020-01-29 06:55:23 +01:00
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 {
2020-12-27 04:34:19 +01:00
notification . NotifyMigrateRepository ( ctx . User , repoOwner , repo )
return
2020-01-29 06:55:23 +01:00
}
if repo != nil {
2020-09-11 00:29:19 +02:00
if errDelete := models . DeleteRepository ( ctx . User , repoOwner . ID , repo . ID ) ; errDelete != nil {
2020-01-29 06:55:23 +01:00
log . Error ( "DeleteRepository: %v" , errDelete )
}
}
} ( )
2021-06-17 00:02:24 +02:00
if _ , err = migrations . MigrateRepository ( graceful . GetManager ( ) . HammerContext ( ) , ctx . User , repoOwner . Name , opts , nil ) ; err != nil {
2020-09-11 00:29:19 +02:00
handleMigrateError ( ctx , repoOwner , remoteAddr , err )
2020-01-29 06:55:23 +01:00
return
}
2020-09-11 00:29:19 +02:00
log . Trace ( "Repository migrated: %s/%s" , repoOwner . Name , form . RepoName )
2020-12-02 22:38:30 +01:00
ctx . JSON ( http . StatusCreated , convert . ToRepo ( repo , models . AccessModeAdmin ) )
2020-01-29 06:55:23 +01:00
}
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." )
2020-09-25 06:09:23 +02:00
case models . IsErrRepoFilesAlreadyExist ( err ) :
ctx . Error ( http . StatusConflict , "" , "Files already exist for this repository. Adopt them or delete them." )
2020-01-29 06:55:23 +01:00
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 ) )
2020-02-23 20:52:05 +01:00
case models . IsErrNameCharsNotAllowed ( err ) :
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "The username '%s' contains invalid characters." , err . ( models . ErrNameCharsNotAllowed ) . Name ) )
2020-01-29 06:55:23 +01:00
case models . IsErrNamePatternNotAllowed ( err ) :
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "The pattern '%s' is not allowed in a username." , err . ( models . ErrNamePatternNotAllowed ) . Pattern ) )
2021-03-15 22:52:11 +01:00
case models . IsErrInvalidCloneAddr ( err ) :
2020-11-29 01:37:58 +01:00
ctx . Error ( http . StatusUnprocessableEntity , "" , err )
2021-01-21 20:33:58 +01:00
case base . IsErrNotSupported ( err ) :
ctx . Error ( http . StatusUnprocessableEntity , "" , err )
2020-01-29 06:55:23 +01:00
default :
2021-06-14 19:20:43 +02:00
err = util . NewStringURLSanitizedError ( err , remoteAddr , true )
2020-01-29 06:55:23 +01:00
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 )
}
}
}
2021-04-09 00:25:57 +02:00
func handleRemoteAddrError ( ctx * context . APIContext , err error ) {
if models . IsErrInvalidCloneAddr ( err ) {
addrErr := err . ( * models . ErrInvalidCloneAddr )
switch {
case addrErr . IsURLError :
ctx . Error ( http . StatusUnprocessableEntity , "" , err )
case addrErr . IsPermissionDenied :
if addrErr . LocalPath {
ctx . Error ( http . StatusUnprocessableEntity , "" , "You are not allowed to import local repositories." )
} else if len ( addrErr . PrivateNet ) == 0 {
ctx . Error ( http . StatusUnprocessableEntity , "" , "You are not allowed to import from blocked hosts." )
} else {
ctx . Error ( http . StatusUnprocessableEntity , "" , "You are not allowed to import from private IPs." )
}
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 )
}
}