2024-03-13 18:07:53 +08:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"net/url"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
)
const (
tplFork base . TplName = "repo/pulls/fork"
)
func getForkRepository ( ctx * context . Context ) * repo_model . Repository {
forkRepo := ctx . Repo . Repository
if ctx . Written ( ) {
return nil
}
if forkRepo . IsEmpty {
log . Trace ( "Empty repository %-v" , forkRepo )
ctx . NotFound ( "getForkRepository" , nil )
return nil
}
if err := forkRepo . LoadOwner ( ctx ) ; err != nil {
ctx . ServerError ( "LoadOwner" , err )
return nil
}
ctx . Data [ "repo_name" ] = forkRepo . Name
ctx . Data [ "description" ] = forkRepo . Description
ctx . Data [ "IsPrivate" ] = forkRepo . IsPrivate || forkRepo . Owner . Visibility == structs . VisibleTypePrivate
canForkToUser := forkRepo . OwnerID != ctx . Doer . ID && ! repo_model . HasForkedRepo ( ctx , ctx . Doer . ID , forkRepo . ID )
ctx . Data [ "ForkRepo" ] = forkRepo
ownedOrgs , err := organization . GetOrgsCanCreateRepoByUserID ( ctx , ctx . Doer . ID )
if err != nil {
ctx . ServerError ( "GetOrgsCanCreateRepoByUserID" , err )
return nil
}
var orgs [ ] * organization . Organization
for _ , org := range ownedOrgs {
if forkRepo . OwnerID != org . ID && ! repo_model . HasForkedRepo ( ctx , org . ID , forkRepo . ID ) {
orgs = append ( orgs , org )
}
}
traverseParentRepo := forkRepo
for {
if ctx . Doer . ID == traverseParentRepo . OwnerID {
canForkToUser = false
} else {
for i , org := range orgs {
if org . ID == traverseParentRepo . OwnerID {
orgs = append ( orgs [ : i ] , orgs [ i + 1 : ] ... )
break
}
}
}
if ! traverseParentRepo . IsFork {
break
}
traverseParentRepo , err = repo_model . GetRepositoryByID ( ctx , traverseParentRepo . ForkID )
if err != nil {
ctx . ServerError ( "GetRepositoryByID" , err )
return nil
}
}
ctx . Data [ "CanForkToUser" ] = canForkToUser
ctx . Data [ "Orgs" ] = orgs
if canForkToUser {
ctx . Data [ "ContextUser" ] = ctx . Doer
} else if len ( orgs ) > 0 {
ctx . Data [ "ContextUser" ] = orgs [ 0 ]
} else {
ctx . Data [ "CanForkRepo" ] = false
ctx . Flash . Error ( ctx . Tr ( "repo.fork_no_valid_owners" ) , true )
return nil
}
branches , err := git_model . FindBranchNames ( ctx , git_model . FindBranchOptions {
2024-03-22 20:53:52 +08:00
RepoID : ctx . Repo . Repository . ID ,
ListOptions : db . ListOptionsAll ,
2024-03-13 18:07:53 +08:00
IsDeletedBranch : optional . Some ( false ) ,
// Add it as the first option
ExcludeBranchNames : [ ] string { ctx . Repo . Repository . DefaultBranch } ,
} )
if err != nil {
ctx . ServerError ( "FindBranchNames" , err )
return nil
}
ctx . Data [ "Branches" ] = append ( [ ] string { ctx . Repo . Repository . DefaultBranch } , branches ... )
return forkRepo
}
// Fork render repository fork page
func Fork ( ctx * context . Context ) {
ctx . Data [ "Title" ] = ctx . Tr ( "new_fork" )
if ctx . Doer . CanForkRepo ( ) {
ctx . Data [ "CanForkRepo" ] = true
} else {
maxCreationLimit := ctx . Doer . MaxCreationLimit ( )
msg := ctx . TrN ( maxCreationLimit , "repo.form.reach_limit_of_creation_1" , "repo.form.reach_limit_of_creation_n" , maxCreationLimit )
ctx . Flash . Error ( msg , true )
}
getForkRepository ( ctx )
if ctx . Written ( ) {
return
}
ctx . HTML ( http . StatusOK , tplFork )
}
// ForkPost response for forking a repository
func ForkPost ( ctx * context . Context ) {
form := web . GetForm ( ctx ) . ( * forms . CreateRepoForm )
ctx . Data [ "Title" ] = ctx . Tr ( "new_fork" )
ctx . Data [ "CanForkRepo" ] = true
ctxUser := checkContextUser ( ctx , form . UID )
if ctx . Written ( ) {
return
}
forkRepo := getForkRepository ( ctx )
if ctx . Written ( ) {
return
}
ctx . Data [ "ContextUser" ] = ctxUser
if ctx . HasError ( ) {
ctx . HTML ( http . StatusOK , tplFork )
return
}
var err error
traverseParentRepo := forkRepo
for {
if ctxUser . ID == traverseParentRepo . OwnerID {
ctx . RenderWithErr ( ctx . Tr ( "repo.settings.new_owner_has_same_repo" ) , tplFork , & form )
return
}
repo := repo_model . GetForkedRepo ( ctx , ctxUser . ID , traverseParentRepo . ID )
if repo != nil {
ctx . Redirect ( ctxUser . HomeLink ( ) + "/" + url . PathEscape ( repo . Name ) )
return
}
if ! traverseParentRepo . IsFork {
break
}
traverseParentRepo , err = repo_model . GetRepositoryByID ( ctx , traverseParentRepo . ForkID )
if err != nil {
ctx . ServerError ( "GetRepositoryByID" , err )
return
}
}
// Check if user is allowed to create repo's on the organization.
if ctxUser . IsOrganization ( ) {
isAllowedToFork , err := organization . OrgFromUser ( ctxUser ) . CanCreateOrgRepo ( ctx , ctx . Doer . ID )
if err != nil {
ctx . ServerError ( "CanCreateOrgRepo" , err )
return
} else if ! isAllowedToFork {
ctx . Error ( http . StatusForbidden )
return
}
}
repo , err := repo_service . ForkRepository ( ctx , ctx . Doer , ctxUser , repo_service . ForkRepoOptions {
BaseRepo : forkRepo ,
Name : form . RepoName ,
Description : form . Description ,
SingleBranch : form . ForkSingleBranch ,
} )
if err != nil {
ctx . Data [ "Err_RepoName" ] = true
switch {
case repo_model . IsErrReachLimitOfRepo ( err ) :
maxCreationLimit := ctxUser . MaxCreationLimit ( )
msg := ctx . TrN ( maxCreationLimit , "repo.form.reach_limit_of_creation_1" , "repo.form.reach_limit_of_creation_n" , maxCreationLimit )
ctx . RenderWithErr ( msg , tplFork , & form )
case repo_model . IsErrRepoAlreadyExist ( err ) :
ctx . RenderWithErr ( ctx . Tr ( "repo.settings.new_owner_has_same_repo" ) , tplFork , & form )
case repo_model . IsErrRepoFilesAlreadyExist ( err ) :
switch {
case ctx . IsUserSiteAdmin ( ) || ( setting . Repository . AllowAdoptionOfUnadoptedRepositories && setting . Repository . AllowDeleteOfUnadoptedRepositories ) :
ctx . RenderWithErr ( ctx . Tr ( "form.repository_files_already_exist.adopt_or_delete" ) , tplFork , form )
case setting . Repository . AllowAdoptionOfUnadoptedRepositories :
ctx . RenderWithErr ( ctx . Tr ( "form.repository_files_already_exist.adopt" ) , tplFork , form )
case setting . Repository . AllowDeleteOfUnadoptedRepositories :
ctx . RenderWithErr ( ctx . Tr ( "form.repository_files_already_exist.delete" ) , tplFork , form )
default :
ctx . RenderWithErr ( ctx . Tr ( "form.repository_files_already_exist" ) , tplFork , form )
}
case db . IsErrNameReserved ( err ) :
ctx . RenderWithErr ( ctx . Tr ( "repo.form.name_reserved" , err . ( db . ErrNameReserved ) . Name ) , tplFork , & form )
case db . IsErrNamePatternNotAllowed ( err ) :
ctx . RenderWithErr ( ctx . Tr ( "repo.form.name_pattern_not_allowed" , err . ( db . ErrNamePatternNotAllowed ) . Pattern ) , tplFork , & form )
case errors . Is ( err , user_model . ErrBlockedUser ) :
ctx . RenderWithErr ( ctx . Tr ( "repo.fork.blocked_user" ) , tplFork , form )
default :
ctx . ServerError ( "ForkPost" , err )
}
return
}
log . Trace ( "Repository forked[%d]: %s/%s" , forkRepo . ID , ctxUser . Name , repo . Name )
ctx . Redirect ( ctxUser . HomeLink ( ) + "/" + url . PathEscape ( repo . Name ) )
}