code.gitea.io/gitea@v1.22.3/routers/web/repo/fork.go (about) 1 // Copyright 2024 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "errors" 8 "net/http" 9 "net/url" 10 11 "code.gitea.io/gitea/models/db" 12 git_model "code.gitea.io/gitea/models/git" 13 "code.gitea.io/gitea/models/organization" 14 repo_model "code.gitea.io/gitea/models/repo" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/base" 17 "code.gitea.io/gitea/modules/log" 18 "code.gitea.io/gitea/modules/optional" 19 "code.gitea.io/gitea/modules/setting" 20 "code.gitea.io/gitea/modules/structs" 21 "code.gitea.io/gitea/modules/web" 22 "code.gitea.io/gitea/services/context" 23 "code.gitea.io/gitea/services/forms" 24 repo_service "code.gitea.io/gitea/services/repository" 25 ) 26 27 const ( 28 tplFork base.TplName = "repo/pulls/fork" 29 ) 30 31 func getForkRepository(ctx *context.Context) *repo_model.Repository { 32 forkRepo := ctx.Repo.Repository 33 if ctx.Written() { 34 return nil 35 } 36 37 if forkRepo.IsEmpty { 38 log.Trace("Empty repository %-v", forkRepo) 39 ctx.NotFound("getForkRepository", nil) 40 return nil 41 } 42 43 if err := forkRepo.LoadOwner(ctx); err != nil { 44 ctx.ServerError("LoadOwner", err) 45 return nil 46 } 47 48 ctx.Data["repo_name"] = forkRepo.Name 49 ctx.Data["description"] = forkRepo.Description 50 ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate 51 canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) 52 53 ctx.Data["ForkRepo"] = forkRepo 54 55 ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID) 56 if err != nil { 57 ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) 58 return nil 59 } 60 var orgs []*organization.Organization 61 for _, org := range ownedOrgs { 62 if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) { 63 orgs = append(orgs, org) 64 } 65 } 66 67 traverseParentRepo := forkRepo 68 for { 69 if ctx.Doer.ID == traverseParentRepo.OwnerID { 70 canForkToUser = false 71 } else { 72 for i, org := range orgs { 73 if org.ID == traverseParentRepo.OwnerID { 74 orgs = append(orgs[:i], orgs[i+1:]...) 75 break 76 } 77 } 78 } 79 80 if !traverseParentRepo.IsFork { 81 break 82 } 83 traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) 84 if err != nil { 85 ctx.ServerError("GetRepositoryByID", err) 86 return nil 87 } 88 } 89 90 ctx.Data["CanForkToUser"] = canForkToUser 91 ctx.Data["Orgs"] = orgs 92 93 if canForkToUser { 94 ctx.Data["ContextUser"] = ctx.Doer 95 } else if len(orgs) > 0 { 96 ctx.Data["ContextUser"] = orgs[0] 97 } else { 98 ctx.Data["CanForkRepo"] = false 99 ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true) 100 return nil 101 } 102 103 branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ 104 RepoID: ctx.Repo.Repository.ID, 105 ListOptions: db.ListOptionsAll, 106 IsDeletedBranch: optional.Some(false), 107 // Add it as the first option 108 ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch}, 109 }) 110 if err != nil { 111 ctx.ServerError("FindBranchNames", err) 112 return nil 113 } 114 ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) 115 116 return forkRepo 117 } 118 119 // Fork render repository fork page 120 func Fork(ctx *context.Context) { 121 ctx.Data["Title"] = ctx.Tr("new_fork") 122 123 if ctx.Doer.CanForkRepo() { 124 ctx.Data["CanForkRepo"] = true 125 } else { 126 maxCreationLimit := ctx.Doer.MaxCreationLimit() 127 msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) 128 ctx.Flash.Error(msg, true) 129 } 130 131 getForkRepository(ctx) 132 if ctx.Written() { 133 return 134 } 135 136 ctx.HTML(http.StatusOK, tplFork) 137 } 138 139 // ForkPost response for forking a repository 140 func ForkPost(ctx *context.Context) { 141 form := web.GetForm(ctx).(*forms.CreateRepoForm) 142 ctx.Data["Title"] = ctx.Tr("new_fork") 143 ctx.Data["CanForkRepo"] = true 144 145 ctxUser := checkContextUser(ctx, form.UID) 146 if ctx.Written() { 147 return 148 } 149 150 forkRepo := getForkRepository(ctx) 151 if ctx.Written() { 152 return 153 } 154 155 ctx.Data["ContextUser"] = ctxUser 156 157 if ctx.HasError() { 158 ctx.HTML(http.StatusOK, tplFork) 159 return 160 } 161 162 var err error 163 traverseParentRepo := forkRepo 164 for { 165 if ctxUser.ID == traverseParentRepo.OwnerID { 166 ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) 167 return 168 } 169 repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) 170 if repo != nil { 171 ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) 172 return 173 } 174 if !traverseParentRepo.IsFork { 175 break 176 } 177 traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) 178 if err != nil { 179 ctx.ServerError("GetRepositoryByID", err) 180 return 181 } 182 } 183 184 // Check if user is allowed to create repo's on the organization. 185 if ctxUser.IsOrganization() { 186 isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID) 187 if err != nil { 188 ctx.ServerError("CanCreateOrgRepo", err) 189 return 190 } else if !isAllowedToFork { 191 ctx.Error(http.StatusForbidden) 192 return 193 } 194 } 195 196 repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ 197 BaseRepo: forkRepo, 198 Name: form.RepoName, 199 Description: form.Description, 200 SingleBranch: form.ForkSingleBranch, 201 }) 202 if err != nil { 203 ctx.Data["Err_RepoName"] = true 204 switch { 205 case repo_model.IsErrReachLimitOfRepo(err): 206 maxCreationLimit := ctxUser.MaxCreationLimit() 207 msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) 208 ctx.RenderWithErr(msg, tplFork, &form) 209 case repo_model.IsErrRepoAlreadyExist(err): 210 ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) 211 case repo_model.IsErrRepoFilesAlreadyExist(err): 212 switch { 213 case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): 214 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form) 215 case setting.Repository.AllowAdoptionOfUnadoptedRepositories: 216 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) 217 case setting.Repository.AllowDeleteOfUnadoptedRepositories: 218 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) 219 default: 220 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) 221 } 222 case db.IsErrNameReserved(err): 223 ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) 224 case db.IsErrNamePatternNotAllowed(err): 225 ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) 226 case errors.Is(err, user_model.ErrBlockedUser): 227 ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) 228 default: 229 ctx.ServerError("ForkPost", err) 230 } 231 return 232 } 233 234 log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) 235 ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) 236 }