mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-31 03:14:17 +09:00
With `[repository] ALLOW_FORK_INTO_SAME_OWNER = true` a user can fork their own repository, but an organization still cannot fork a repository it owns. The fork button check and the owner list on the fork page compared the organization with the repository owner directly instead of asking `CanUserForkBetweenOwners`, and the walk over parent repositories dropped the owning organization whenever the setting was on. Organizations now follow the setting the same way users already do, and nothing changes when it is off. Fixes https://github.com/go-gitea/gitea/issues/40021 Assisted-by: Claude Code:claude-opus-5-5
238 lines
7.0 KiB
Go
238 lines
7.0 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
|
|
"gitea.dev/models/db"
|
|
git_model "gitea.dev/models/git"
|
|
"gitea.dev/models/organization"
|
|
repo_model "gitea.dev/models/repo"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/optional"
|
|
"gitea.dev/modules/repository"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/structs"
|
|
"gitea.dev/modules/templates"
|
|
"gitea.dev/modules/web"
|
|
"gitea.dev/services/context"
|
|
"gitea.dev/services/forms"
|
|
repo_service "gitea.dev/services/repository"
|
|
)
|
|
|
|
const (
|
|
tplFork templates.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(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 := repository.CanUserForkBetweenOwners(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 repository.CanUserForkBetweenOwners(forkRepo.OwnerID, org.ID) && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
|
|
orgs = append(orgs, org)
|
|
}
|
|
}
|
|
|
|
traverseParentRepo := forkRepo
|
|
for {
|
|
if !repository.CanUserForkBetweenOwners(ctx.Doer.ID, traverseParentRepo.OwnerID) {
|
|
canForkToUser = false
|
|
}
|
|
orgs = slices.DeleteFunc(orgs, func(org *organization.Organization) bool {
|
|
return !repository.CanUserForkBetweenOwners(org.ID, traverseParentRepo.OwnerID)
|
|
})
|
|
|
|
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
|
|
|
|
// TODO: this message should only be shown for the "current doer" when it is selected, just like the "new repo" page.
|
|
// msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", ctx.Doer.MaxCreationLimit())
|
|
|
|
if canForkToUser {
|
|
ctx.Data["ContextUser"] = ctx.Doer
|
|
ctx.Data["CanForkRepoInNewOwner"] = true
|
|
} else if len(orgs) > 0 {
|
|
ctx.Data["ContextUser"] = orgs[0]
|
|
ctx.Data["CanForkRepoInNewOwner"] = true
|
|
} else {
|
|
ctx.Data["CanForkRepoInNewOwner"] = false
|
|
ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
|
|
return nil
|
|
}
|
|
|
|
branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
ListOptions: db.ListOptionsAll,
|
|
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")
|
|
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[*forms.CreateRepoForm](ctx)
|
|
ctx.Data["Title"] = ctx.Tr("new_fork")
|
|
|
|
ctxUser := checkContextUser(ctx, form.UID)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
forkRepo := getForkRepository(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
ctx.Data["ContextUser"] = ctxUser
|
|
|
|
if ctx.HasError() {
|
|
ctx.JSONError(ctx.GetErrMsg())
|
|
return
|
|
}
|
|
|
|
var err error
|
|
traverseParentRepo := forkRepo
|
|
for {
|
|
if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
|
|
ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
|
|
return
|
|
}
|
|
repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
|
|
if repo != nil {
|
|
ctx.JSONRedirect(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.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
|
|
BaseRepo: forkRepo,
|
|
Name: form.RepoName,
|
|
Description: form.Description,
|
|
SingleBranch: form.ForkSingleBranch,
|
|
})
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
|
|
}
|
|
|
|
func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
|
|
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
|
|
if err != nil {
|
|
ctx.Data["Err_RepoName"] = true
|
|
var errNameReserved db.ErrNameReserved
|
|
var errNamePatternNotAllowed db.ErrNamePatternNotAllowed
|
|
switch {
|
|
case repo_model.IsErrReachLimitOfRepo(err):
|
|
maxCreationLimit := owner.MaxCreationLimit()
|
|
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
|
|
ctx.JSONError(msg)
|
|
case repo_model.IsErrRepoAlreadyExist(err):
|
|
ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
|
|
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
|
switch {
|
|
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
|
|
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
|
|
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
|
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
|
|
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
|
ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
|
|
default:
|
|
ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
|
|
}
|
|
case errors.As(err, &errNameReserved):
|
|
ctx.JSONError(ctx.Tr("repo.form.name_reserved", errNameReserved.Name))
|
|
case errors.As(err, &errNamePatternNotAllowed):
|
|
ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", errNamePatternNotAllowed.Pattern))
|
|
case errors.Is(err, user_model.ErrBlockedUser):
|
|
ctx.JSONError(ctx.Tr("repo.fork.blocked_user"))
|
|
default:
|
|
ctx.ServerError("ForkPost", err)
|
|
}
|
|
return nil
|
|
}
|
|
return repo
|
|
}
|