mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
239 lines
7.1 KiB
Go
239 lines
7.1 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
git_model "github.com/hanzoai/git/models/git"
|
|
"github.com/hanzoai/git/models/organization"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/optional"
|
|
"github.com/hanzoai/git/modules/repository"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/templates"
|
|
"github.com/hanzoai/git/modules/web"
|
|
"github.com/hanzoai/git/services/context"
|
|
"github.com/hanzoai/git/services/forms"
|
|
repo_service "github.com/hanzoai/git/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 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
|
|
} 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
|
|
|
|
// 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(ctx).(*forms.CreateRepoForm)
|
|
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
|
|
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 db.IsErrNameReserved(err):
|
|
ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name))
|
|
case db.IsErrNamePatternNotAllowed(err):
|
|
ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.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
|
|
}
|