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  }