From f834dca87315f32955c00c98fc3e15d48b0fafd1 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Fri, 18 Apr 2025 03:26:24 +0200 Subject: [PATCH 01/22] Edit file workflow for creating a fork and proposing changes When viewing a file that the user can't edit because they can't write to the branch, the edit button is no longer disabled. If no user fork of the repository exists, there is now a page create one. It will automatically create a fork with a single branch matching the one being viewed, and a unique repository name will be automatically picked. If the fork exists, an message will explain at the top of the edit page explaining that the changes will be applied to a branch in the fork. The base repository branch will be pushed to a new branch to the fork, and then the edits will be applied on top. This all happens when accessing /_edit/, so that for example online documentation can have an "edit this page" link to the base repository that does the right thing. --- options/locale/locale_en-US.ini | 11 +- routers/web/repo/cherry_pick.go | 6 +- routers/web/repo/editor.go | 264 +++++++++++++++++++++++++++----- routers/web/repo/editor_test.go | 2 +- routers/web/repo/fork.go | 44 ++++-- routers/web/repo/patch.go | 6 +- routers/web/repo/view_file.go | 3 +- routers/web/repo/view_readme.go | 6 +- routers/web/web.go | 5 +- services/forms/repo_form.go | 11 ++ templates/repo/editor/edit.tmpl | 5 + templates/repo/editor/fork.tmpl | 40 +++++ 12 files changed, 337 insertions(+), 66 deletions(-) create mode 100644 templates/repo/editor/fork.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 54089be24a1c2..7f57ccbf44378 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1334,7 +1334,6 @@ editor.cannot_edit_non_text_files = Binary files cannot be edited in the web int editor.edit_this_file = Edit File editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. -editor.fork_before_edit = You must fork this repository to make or propose changes to this file. editor.delete_this_file = Delete File editor.must_have_write_access = You must have write access to make or propose changes to this file. editor.file_delete_success = File "%s" has been deleted. @@ -1394,6 +1393,16 @@ editor.user_no_push_to_branch = User cannot push to branch editor.require_signed_commit = Branch requires a signed commit editor.cherry_pick = Cherry-pick %s onto: editor.revert = Revert %s onto: +editor.fork_create = Fork Repository to Propose Changes +editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request. +editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request. +editor.fork_failed_to_push_branch = Failed to push branch %s to your repoitory. +editor.cannot_find_editable_repo = Can not find repository to apply the edit to. Was it deleted while editing? +editor.fork_not_editable = Fork Repository Not Editable +editor.fork_internal_error = Internal error loading repository information about <b>%s</b>. +editor.fork_is_archived = Your repository <b>%s</b> is archived. Unarchive it in repository settings to make changes. +editor.fork_code_disabled = Code is disabled in your repository <b>%s</b>. Enable code in repository settings to make changes. +editor.fork_no_permission = You do not have permission to write to repository <b>%s</b>. commits.desc = Browse source code change history. commits.commits = Commits diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 690b830bc2f2a..bc1fd1c14e69f 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -47,7 +47,7 @@ func CherryPick(ctx *context.Context) { ctx.Data["commit_message"] = splits[1] } - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) ctx.Data["TreePath"] = "" if canCommit { @@ -55,7 +55,7 @@ func CherryPick(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() @@ -75,7 +75,7 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["CherryPickType"] = "cherry-pick" } - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index c925b6115147a..9788fc35db85b 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -11,13 +11,16 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" @@ -27,6 +30,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" + repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -35,42 +39,55 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" + tplForkFile templates.TplName = "repo/editor/fork" frmCommitChoiceDirect string = "direct" frmCommitChoiceNewBranch string = "commit-to-new-branch" ) -func canCreateBasePullRequest(ctx *context.Context) bool { - baseRepo := ctx.Repo.Repository.BaseRepo +func canCreateBasePullRequest(ctx *context.Context, editRepo *repo_model.Repository) bool { + baseRepo := editRepo.BaseRepo return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests) } -func renderCommitRights(ctx *context.Context) bool { - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) - if err != nil { - log.Error("CanCommitToBranch: %v", err) - } - ctx.Data["CanCommitToBranch"] = canCommitToBranch - ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx) +func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) bool { + if editRepo == ctx.Repo.Repository { + // Editing the same repository that we are viewing + canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) + if err != nil { + log.Error("CanCommitToBranch: %v", err) + } + + ctx.Data["CanCommitToBranch"] = canCommitToBranch + ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx, editRepo) + + return canCommitToBranch.CanCommitToBranch + } else { + // Editing a user fork of the repository we are viewing + ctx.Data["CanCommitToBranch"] = context.CanCommitToBranchResults{} + ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) - return canCommitToBranch.CanCommitToBranch + return false + } } // redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) { +func redirectForCommitChoice(ctx *context.Context, editRepo *repo_model.Repository, commitChoice, newBranchName, treePath string) { if commitChoice == frmCommitChoiceNewBranch { // Redirect to a pull request when possible redirectToPullRequest := false - repo := ctx.Repo.Repository + repo := editRepo baseBranch := ctx.Repo.BranchName headBranch := newBranchName - if repo.UnitEnabled(ctx, unit.TypePullRequests) { - redirectToPullRequest = true - } else if canCreateBasePullRequest(ctx) { + if canCreateBasePullRequest(ctx, repo) { + log.Error("Can create base!") redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo + } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { + log.Error("Fallback!") + redirectToPullRequest = true } if redirectToPullRequest { @@ -83,7 +100,7 @@ func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, ctx.RedirectToCurrentSite( returnURI, - ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), + editRepo.Link()+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), ) } @@ -102,7 +119,57 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } -func editFileCommon(ctx *context.Context, isNewFile bool) { +// editFileCommon setup common context, and return repository that will be edited. +// If no repository can be found for editing, nil is returned along with a message +// explaining why editing is not possible. +func editFileCommon(ctx *context.Context, isNewFile bool) (*repo_model.Repository, any) { + editRepo := ctx.Repo.Repository + + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + // If we can't write to the branch, try find a user fork to create a branch in instead + userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + if err != nil { + log.Error("GetUserFork: %v", err) + return nil, nil + } + if userRepo == nil { + return nil, nil + } + + // Load repository information + if err := userRepo.LoadOwner(ctx); err != nil { + log.Error("LoadOwner: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { + if err != nil { + log.Error("GetBaseRepo: %v", err) + } else { + log.Error("GetBaseRepo: Expected a base repo for user fork", err) + } + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + + // Check permissions, code unit and archiving + permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) + if err != nil { + log.Error("access_model.GetUserRepoPermission: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if !permission.CanWrite(unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) + } + if !userRepo.UnitEnabled(ctx, unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) + } + if userRepo.IsArchived { + return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) + } + + editRepo = userRepo + ctx.Data["ForkRepo"] = userRepo + } + ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() @@ -110,11 +177,70 @@ func editFileCommon(ctx *context.Context, isNewFile bool) { ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" ctx.Data["ReturnURI"] = ctx.FormString("return_uri") + + return editRepo, "" +} + +func forkToEditFileCommon(ctx *context.Context, treePath string, notEditableMessage any) { + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + filePath, _ := path.Split(ctx.Req.URL.Query().Get("filename")) + filePath = strings.Trim(filePath, "/") + treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) + + ctx.Data["TreePath"] = treePath + ctx.Data["TreeNames"] = treeNames + ctx.Data["TreePaths"] = treePaths + ctx.Data["CanForkRepo"] = notEditableMessage == nil + ctx.Data["NotEditableMessage"] = notEditableMessage +} + +func forkToEditFile(ctx *context.Context, notEditableMessage any) { + // Suggest to create a new user fork + forkToEditFileCommon(ctx, ctx.Repo.TreePath, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) +} + +func forkToEditFilePost(ctx *context.Context, form forms.ForkToEditRepoFileForm) { + editRepo, notEditableMessage := editFileCommon(ctx, false) + + ctx.Data["PageHasPosted"] = true + + // Fork repository, if it doesn't already exist + if editRepo == nil && notEditableMessage == nil { + newRepo, err := ForkRepository(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: GetUniqueRepositoryName(ctx, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.BranchName, + }, tplForkFile, form) + if err != nil { + forkToEditFileCommon(ctx, form.TreePath, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) + return + } + + editRepo = newRepo + } + + // Redirect back to editing page + ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(form.TreePath))) +} + +func ForkToEditFilePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) + forkToEditFilePost(ctx, *form) } func editFile(ctx *context.Context, isNewFile bool) { - editFileCommon(ctx, isNewFile) - canCommit := renderCommitRights(ctx) + editRepo, notEditableMessage := editFileCommon(ctx, isNewFile) + if editRepo == nil { + // No editable repository, suggest to create a fork + forkToEditFile(ctx, notEditableMessage) + return + } + + canCommit := renderCommitRights(ctx, editRepo) treePath := cleanUploadFileName(ctx.Repo.TreePath) if treePath != ctx.Repo.TreePath { @@ -190,7 +316,7 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch) - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, editRepo) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) @@ -221,11 +347,29 @@ func NewFile(ctx *context.Context) { editFile(ctx, true) } +// Push branch to user fork, to apply changes on top of +func pushBranchToUserRepo(ctx *context.Context, userRepo *repo_model.Repository, userBranchName string) (err error) { + baseRepo := ctx.Repo.Repository + baseBranchName := ctx.Repo.BranchName + + log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, userRepo.FullName(), userBranchName) + + if err := git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: userRepo.RepoPath(), + Branch: baseBranchName + ":" + userBranchName, + Env: repo_module.PushingEnvironment(ctx.Doer, userRepo), + }); err != nil { + return err + } + + return nil +} + func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editFileCommon(ctx, isNewFile) + editRepo, _ := editFileCommon(ctx, isNewFile) + ctx.Data["PageHasPosted"] = true - canCommit := renderCommitRights(ctx) treeNames, treePaths := getParentTreeFields(form.TreePath) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { @@ -248,8 +392,16 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } + if editRepo == nil { + // No editable repo, maybe the fork was deleted in the meantime + ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tplEditFile, &form) + return + } + + canCommit := renderCommitRights(ctx, editRepo) + // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { + if editRepo == ctx.Repo.Repository && branchName == ctx.Repo.BranchName && !canCommit { ctx.Data["Err_NewBranchName"] = true ctx.Data["commit_choice"] = frmCommitChoiceNewBranch ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) @@ -283,9 +435,18 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + if editRepo != ctx.Repo.Repository { + // If editing a user fork, first push the branch to that repository + err := pushBranchToUserRepo(ctx, editRepo, branchName) + if err != nil { + ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tplEditFile, &form) + return + } + } + + if _, err := files_service.ChangeRepoFiles(ctx, editRepo, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, + OldBranch: branchName, NewBranch: branchName, Message: message, Files: []*files_service.ChangeRepoFile{ @@ -377,13 +538,21 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } } - if ctx.Repo.Repository.IsEmpty { - if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") + if editRepo.IsEmpty { + editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) + if err != nil { + log.Error("gitrepo.OpenRepository: %v", err) + } else { + if editGitRepo != nil { + if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") + } + } + editGitRepo.Close() } } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) + redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } // EditFilePost response for editing file @@ -441,7 +610,7 @@ func DeleteFile(ctx *context.Context) { } ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" @@ -451,7 +620,7 @@ func DeleteFile(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) ctx.HTML(http.StatusOK, tplDeleteFile) } @@ -459,7 +628,7 @@ func DeleteFile(ctx *context.Context) { // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName @@ -594,14 +763,14 @@ func DeleteFilePost(ctx *context.Context) { } } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath) + redirectForCommitChoice(ctx, ctx.Repo.Repository, form.CommitChoice, branchName, treePath) } // UploadFile render upload file page func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) treePath := cleanUploadFileName(ctx.Repo.TreePath) if treePath != ctx.Repo.TreePath { ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) @@ -625,7 +794,7 @@ func UploadFile(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) ctx.HTML(http.StatusOK, tplUploadFile) } @@ -635,7 +804,7 @@ func UploadFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UploadRepoFileForm) ctx.Data["PageIsUpload"] = true upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) oldBranchName := ctx.Repo.BranchName branchName := oldBranchName @@ -795,7 +964,7 @@ func UploadFilePost(ctx *context.Context) { } } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) + redirectForCommitChoice(ctx, ctx.Repo.Repository, form.CommitChoice, branchName, form.TreePath) } func cleanUploadFileName(name string) string { @@ -870,11 +1039,11 @@ func RemoveUploadFileFromServer(ctx *context.Context) { // It will be in the form of <username>-patch-<num> where <num> is the first branch of this format // that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to // type in the branch name themselves (will be an empty field) -func GetUniquePatchBranchName(ctx *context.Context) string { +func GetUniquePatchBranchName(ctx *context.Context, editRepo *repo_model.Repository) string { prefix := ctx.Doer.LowerName + "-patch-" for i := 1; i <= 1000; i++ { branchName := fmt.Sprintf("%s%d", prefix, i) - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil { + if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err != nil { log.Error("GetUniquePatchBranchName: %v", err) return "" } else if !exist { @@ -884,6 +1053,23 @@ func GetUniquePatchBranchName(ctx *context.Context) string { return "" } +// GetUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func GetUniqueRepositoryName(ctx *context.Context, name string) string { + uniqueName := name + i := 1 + + for { + _, err := repo_model.GetRepositoryByName(ctx, ctx.Doer.ID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } +} + // GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is // deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a // SubTree or it has no entries, we go up one dir and see if we can return the user to that listing. diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 89bf8f309cd5e..c54648c51ed3a 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -51,7 +51,7 @@ func TestGetUniquePatchBranchName(t *testing.T) { defer ctx.Repo.GitRepo.Close() expectedBranchName := "user2-patch-1" - branchName := GetUniquePatchBranchName(ctx) + branchName := GetUniquePatchBranchName(ctx, ctx.Repo.Repository) assert.Equal(t, expectedBranchName, branchName) } diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 79f033659b46c..2928c4650124d 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -125,6 +125,11 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { // Fork render repository fork page func Fork(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_fork") + + redirectTo := ctx.FormString("redirect_to_edit") + ctx.Data["ForkForEdit"] = len(redirectTo) > 0 + ctx.Data["ForkRedirectTo"] = redirectTo + getForkRepository(ctx) if ctx.Written() { return @@ -189,44 +194,53 @@ func ForkPost(ctx *context.Context) { } } - repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ + repo, err := ForkRepository(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, - }) + }, tplFork, form) + if err != nil { + return + } + + ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func ForkRepository(ctx *context.Context, user *user_model.User, opts repo_service.ForkRepoOptions, tpl templates.TplName, form any) (*repo_model.Repository, error) { + repo, err := repo_service.ForkRepository(ctx, ctx.Doer, user, opts) if err != nil { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrReachLimitOfRepo(err): - maxCreationLimit := ctxUser.MaxCreationLimit() + maxCreationLimit := user.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) + ctx.RenderWithErr(msg, tpl, &form) case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tpl, &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) + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, &form) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, &form) case errors.Is(err, user_model.ErrBlockedUser): - ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) + ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tpl, form) default: ctx.ServerError("ForkPost", err) } - return + return repo, err } - log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + log.Trace("Repository forked[%d]: %s/%s", opts.BaseRepo.ID, user.Name, repo.Name) + return repo, err } diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index ca346b7e6c313..da47865116972 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -24,7 +24,7 @@ const ( // NewDiffPatch render create patch page func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) ctx.Data["PageIsPatch"] = true @@ -35,7 +35,7 @@ func NewDiffPatch(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() @@ -47,7 +47,7 @@ func NewDiffPatch(ctx *context.Context) { func NewDiffPatchPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditRepoFileForm) - canCommit := renderCommitRights(ctx) + canCommit := renderCommitRights(ctx, ctx.Repo.Repository) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3df6051975bc4..b9897ef76b0ac 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -255,7 +255,8 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } else if !ctx.Repo.RefFullName.IsBranch() { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + ctx.Data["CanEditFile"] = true + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") } } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 459cf0a616232..72a8e3e1d1555 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -212,7 +212,9 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - ctx.Data["CanEditReadmeFile"] = true + if !fInfo.isLFSFile { + if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) || !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["CanEditReadmeFile"] = true + } } } diff --git a/routers/web/web.go b/routers/web/web.go index f28dc6baa4d62..d6ba64f035e5f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1314,9 +1314,12 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo code m.Group("", func() { m.Group("", func() { - m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) m.Combo("/_edit/*").Get(repo.EditFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) + m.Combo("/_fork_to_edit/*"). + Post(web.Bind(forms.ForkToEditRepoFileForm{}), repo.ForkToEditFilePost) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) + m.Group("", func() { m.Combo("/_new/*").Get(repo.NewFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) m.Combo("/_delete/*").Get(repo.DeleteFile). diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 434274c174bed..306c680ccf966 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -733,6 +733,17 @@ func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) b return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +// ForkToEditRepoFileForm form for forking the repo to edit a file +type ForkToEditRepoFileForm struct { + TreePath string `binding:"Required;MaxSize(500)"` +} + +// Validate validates the fields +func (f *ForkToEditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + // _________ .__ __________.__ __ // \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __ // / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ / diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index ae8a60c20c116..16a5988716327 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -10,6 +10,11 @@ {{.CsrfTokenHtml}} <input type="hidden" name="last_commit" value="{{.last_commit}}"> <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{if .ForkRepo}} + <div class="ui blue message"> + <p>{{ctx.Locale.Tr "repo.editor.fork_edit_description" .ForkRepo.FullName}}</p> + </div> + {{end}} <div class="repo-editor-header"> <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}"> <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl new file mode 100644 index 0000000000000..f25d303d15789 --- /dev/null +++ b/templates/repo/editor/fork.tmpl @@ -0,0 +1,40 @@ +{{template "base/head" .}} +<div role="main" aria-label="{{.Title}}" class="page-content repository file editor edit"> + {{template "repo/header" .}} + <div class="ui container"> + {{template "base/alert" .}} + <form class="ui edit form" method="post" action="{{.RepoLink}}/_fork_to_edit/{{.BranchName | PathEscapeSegments}}"> + {{.CsrfTokenHtml}} + <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + <div class="repo-editor-header"> + <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}"> + <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> + {{$n := len .TreeNames}} + {{$l := Eval $n "-" 1}} + {{range $i, $v := .TreeNames}} + <div class="breadcrumb-divider">/</div> + {{if eq $i $l}} + <span class="section">{{$v}}</span> + {{else}} + <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> + {{end}} + {{end}} + <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> + </div> + </div> + <div class="field center"> + {{if .CanForkRepo}} + <h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3> + <p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p> + <button class="ui primary button"> + {{ctx.Locale.Tr "repo.fork_repo"}} + </button> + {{else}} + <h3>{{ctx.Locale.Tr "repo.editor.fork_not_editable"}}</h3> + <p>{{.NotEditableMessage}}</p> + {{end}} + </div> + </form> + </div> +</div> +{{template "base/footer" .}} From a9c552edf6e0bcbeba02c67d642a76302cfcd245 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Tue, 22 Apr 2025 03:43:56 +0200 Subject: [PATCH 02/22] Fix tests and lint --- routers/web/repo/editor.go | 42 +++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 9788fc35db85b..466b62d8e9cc2 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -62,13 +62,13 @@ func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) b ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx, editRepo) return canCommitToBranch.CanCommitToBranch - } else { - // Editing a user fork of the repository we are viewing - ctx.Data["CanCommitToBranch"] = context.CanCommitToBranchResults{} - ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) - - return false } + + // Editing a user fork of the repository we are viewing + ctx.Data["CanCommitToBranch"] = context.CanCommitToBranchResults{} + ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) + + return false } // redirectForCommitChoice redirects after committing the edit to a branch @@ -150,7 +150,13 @@ func editFileCommon(ctx *context.Context, isNewFile bool) (*repo_model.Repositor return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) } - // Check permissions, code unit and archiving + // Check code unit, archiving and permissions. + if !userRepo.UnitEnabled(ctx, unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) + } + if userRepo.IsArchived { + return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) + } permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) if err != nil { log.Error("access_model.GetUserRepoPermission: %v", err) @@ -159,12 +165,6 @@ func editFileCommon(ctx *context.Context, isNewFile bool) (*repo_model.Repositor if !permission.CanWrite(unit.TypeCode) { return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) } - if !userRepo.UnitEnabled(ctx, unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) - } - if userRepo.IsArchived { - return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) - } editRepo = userRepo ctx.Data["ForkRepo"] = userRepo @@ -208,7 +208,7 @@ func forkToEditFilePost(ctx *context.Context, form forms.ForkToEditRepoFileForm) // Fork repository, if it doesn't already exist if editRepo == nil && notEditableMessage == nil { - newRepo, err := ForkRepository(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + _, err := ForkRepository(ctx, ctx.Doer, repo_service.ForkRepoOptions{ BaseRepo: ctx.Repo.Repository, Name: GetUniqueRepositoryName(ctx, ctx.Repo.Repository.Name), Description: ctx.Repo.Repository.Description, @@ -219,8 +219,6 @@ func forkToEditFilePost(ctx *context.Context, form forms.ForkToEditRepoFileForm) ctx.HTML(http.StatusOK, tplForkFile) return } - - editRepo = newRepo } // Redirect back to editing page @@ -354,15 +352,11 @@ func pushBranchToUserRepo(ctx *context.Context, userRepo *repo_model.Repository, log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, userRepo.FullName(), userBranchName) - if err := git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ Remote: userRepo.RepoPath(), Branch: baseBranchName + ":" + userBranchName, Env: repo_module.PushingEnvironment(ctx.Doer, userRepo), - }); err != nil { - return err - } - - return nil + }) } func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { @@ -435,6 +429,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } + oldBranchName := ctx.Repo.BranchName if editRepo != ctx.Repo.Repository { // If editing a user fork, first push the branch to that repository err := pushBranchToUserRepo(ctx, editRepo, branchName) @@ -442,11 +437,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tplEditFile, &form) return } + oldBranchName = branchName } if _, err := files_service.ChangeRepoFiles(ctx, editRepo, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, - OldBranch: branchName, + OldBranch: oldBranchName, NewBranch: branchName, Message: message, Files: []*files_service.ChangeRepoFile{ From 6d23290a03b06449b83e13a5e54b808e5118359f Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Tue, 22 Apr 2025 13:16:51 +0200 Subject: [PATCH 03/22] Also support new/upload/patch/delete operations, refactor permissions --- models/repo/repo.go | 2 +- routers/web/repo/editor.go | 362 ++++++++++++++++++------------ routers/web/repo/patch.go | 34 ++- routers/web/repo/repo.go | 15 -- routers/web/repo/view_file.go | 9 +- routers/web/repo/view_readme.go | 2 +- routers/web/web.go | 28 +-- services/context/permission.go | 6 +- services/context/repo.go | 29 ++- services/forms/repo_form.go | 3 +- templates/repo/editor/delete.tmpl | 5 + templates/repo/editor/fork.tmpl | 1 + templates/repo/editor/patch.tmpl | 5 + templates/repo/editor/upload.tmpl | 5 + templates/repo/empty.tmpl | 2 +- templates/repo/view_content.tmpl | 2 +- 16 files changed, 303 insertions(+), 207 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 2977dfb9f1d8a..6e2727f7e76e6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -644,7 +644,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { // CanEnableEditor returns true if repository meets the requirements of web editor. func (repo *Repository) CanEnableEditor() bool { - return !repo.IsMirror + return !repo.IsMirror && !repo.IsArchived } // DescriptionHTML does special handles to description and return HTML string. diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 466b62d8e9cc2..f9d6a2de97699 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -80,13 +80,11 @@ func redirectForCommitChoice(ctx *context.Context, editRepo *repo_model.Reposito baseBranch := ctx.Repo.BranchName headBranch := newBranchName if canCreateBasePullRequest(ctx, repo) { - log.Error("Can create base!") redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { - log.Error("Fallback!") redirectToPullRequest = true } @@ -119,75 +117,160 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } -// editFileCommon setup common context, and return repository that will be edited. -// If no repository can be found for editing, nil is returned along with a message -// explaining why editing is not possible. -func editFileCommon(ctx *context.Context, isNewFile bool) (*repo_model.Repository, any) { - editRepo := ctx.Repo.Repository +// getEditRepository returns the repository where the actual edits will be written to. +// This may be a fork of the repository owned by the user. If no repository can be found +// for editing, nil is returned along with a message explaining why editing is not possible. +func getEditRepository(ctx *context.Context) (*repo_model.Repository, any) { + if ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + return ctx.Repo.Repository, nil + } + + // If we can't write to the branch, try find a user fork to create a branch in instead + userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + if err != nil { + log.Error("GetUserFork: %v", err) + return nil, nil + } + if userRepo == nil { + return nil, nil + } - if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - // If we can't write to the branch, try find a user fork to create a branch in instead - userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + // Load repository information + if err := userRepo.LoadOwner(ctx); err != nil { + log.Error("LoadOwner: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { if err != nil { - log.Error("GetUserFork: %v", err) - return nil, nil - } - if userRepo == nil { - return nil, nil + log.Error("GetBaseRepo: %v", err) + } else { + log.Error("GetBaseRepo: Expected a base repo for user fork", err) } + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } - // Load repository information - if err := userRepo.LoadOwner(ctx); err != nil { - log.Error("LoadOwner: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } - if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { - if err != nil { - log.Error("GetBaseRepo: %v", err) - } else { - log.Error("GetBaseRepo: Expected a base repo for user fork", err) - } - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } + // Check code unit, archiving and permissions. + if !userRepo.UnitEnabled(ctx, unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) + } + if userRepo.IsArchived { + return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) + } + permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) + if err != nil { + log.Error("access_model.GetUserRepoPermission: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if !permission.CanWrite(unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) + } - // Check code unit, archiving and permissions. - if !userRepo.UnitEnabled(ctx, unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) - } - if userRepo.IsArchived { - return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) - } - permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) - if err != nil { - log.Error("access_model.GetUserRepoPermission: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } - if !permission.CanWrite(unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) + ctx.Data["ForkRepo"] = userRepo + return userRepo, nil +} + +// GetEditRepository returns the repository where the edits will be written to. +// If no repository is editable, redirects to a page to create a fork. +func GetEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_model.Repository { + editRepo, notEditableMessage := getEditRepository(ctx) + if editRepo != nil { + return editRepo + } + + // No editable repository, suggest to create a fork + forkToEditFileCommon(ctx, ctx.Repo.TreePath, editOperation, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) + return nil +} + +// GetEditRepository returns the repository where the edits will be written to. +// If no repository is editable, display an error. +func GetEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form any) *repo_model.Repository { + editRepo, _ := getEditRepository(ctx) + if editRepo == nil { + // No editable repo, maybe the fork was deleted in the meantime + ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tpl, form) + return nil + } + return editRepo +} + +// CheckPushEditBranch chesk if pushing to the branch in the edit repository is possible, +// and if not renders an error and returns false. +func CheckCanPushEditBranch(ctx *context.Context, editRepo *repo_model.Repository, branchName string, canCommit bool, tpl templates.TplName, form any) bool { + if ctx.Repo.Repository != editRepo || ctx.Repo.BranchName != branchName { + // When pushing to a fork or another branch on the same repository, it should not exist yet + if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { + ctx.Data["Err_NewBranchName"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) + return false } + } else if ctx.Repo.Repository == editRepo && !canCommit { + // When we can't commit to the same branch on the same repository, it's protected + ctx.Data["Err_NewBranchName"] = true + ctx.Data["commit_choice"] = frmCommitChoiceNewBranch + ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tpl, form) + return false + } + + return true +} - editRepo = userRepo - ctx.Data["ForkRepo"] = userRepo +// PushEditBranchOrError pushes the branch that editing will be applied on top of +// to the user fork, if needed. On failure, it displays and returns an error. The +// branch name to be used for editing is returned. +func PushEditBranchOrError(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) (string, error) { + if editRepo == ctx.Repo.Repository { + return ctx.Repo.BranchName, nil } - ctx.Data["PageIsEdit"] = true - ctx.Data["IsNewFile"] = isNewFile - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" - ctx.Data["ReturnURI"] = ctx.FormString("return_uri") + // If editing a user fork, first push the branch to that repository + baseRepo := ctx.Repo.Repository + baseBranchName := ctx.Repo.BranchName + + log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, editRepo.FullName(), branchName) + + if err := git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: editRepo.RepoPath(), + Branch: baseBranchName + ":" + branchName, + Env: repo_module.PushingEnvironment(ctx.Doer, editRepo), + }); err != nil { + ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tpl, form) + return "", nil + } - return editRepo, "" + return branchName, nil } -func forkToEditFileCommon(ctx *context.Context, treePath string, notEditableMessage any) { +// UpdateEditRepositoryIsEmpty updates the the edit repository to mark it as no longer empty +func UpdateEditRepositoryIsEmpty(ctx *context.Context, editRepo *repo_model.Repository) { + if !editRepo.IsEmpty { + return + } + + editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) + if err != nil { + log.Error("gitrepo.OpenRepository: %v", err) + return + } + if editGitRepo == nil { + return + } + + if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") + } + editGitRepo.Close() +} + +func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { // Check if the filename (and additional path) is specified in the querystring // (filename is a misnomer, but kept for compatibility with GitHub) filePath, _ := path.Split(ctx.Req.URL.Query().Get("filename")) filePath = strings.Trim(filePath, "/") treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) + ctx.Data["EditOperation"] = editOperation ctx.Data["TreePath"] = treePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths @@ -195,14 +278,10 @@ func forkToEditFileCommon(ctx *context.Context, treePath string, notEditableMess ctx.Data["NotEditableMessage"] = notEditableMessage } -func forkToEditFile(ctx *context.Context, notEditableMessage any) { - // Suggest to create a new user fork - forkToEditFileCommon(ctx, ctx.Repo.TreePath, notEditableMessage) - ctx.HTML(http.StatusOK, tplForkFile) -} +func ForkToEditFilePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) -func forkToEditFilePost(ctx *context.Context, form forms.ForkToEditRepoFileForm) { - editRepo, notEditableMessage := editFileCommon(ctx, false) + editRepo, notEditableMessage := getEditRepository(ctx) ctx.Data["PageHasPosted"] = true @@ -215,29 +294,37 @@ func forkToEditFilePost(ctx *context.Context, form forms.ForkToEditRepoFileForm) SingleBranch: ctx.Repo.BranchName, }, tplForkFile, form) if err != nil { - forkToEditFileCommon(ctx, form.TreePath, notEditableMessage) + forkToEditFileCommon(ctx, form.TreePath, form.EditOperation, notEditableMessage) ctx.HTML(http.StatusOK, tplForkFile) return } } // Redirect back to editing page - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(form.TreePath))) + ctx.Redirect(path.Join(ctx.Repo.RepoLink, form.EditOperation, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(form.TreePath))) } -func ForkToEditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) - forkToEditFilePost(ctx, *form) +// editFileCommon setup common context, and return repository that will be edited. +// If no repository can be found for editing, nil is returned along with a message +// explaining why editing is not possible. +func editFileCommon(ctx *context.Context, isNewFile bool) { + ctx.Data["PageIsEdit"] = true + ctx.Data["IsNewFile"] = isNewFile + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") + ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" + ctx.Data["ReturnURI"] = ctx.FormString("return_uri") } func editFile(ctx *context.Context, isNewFile bool) { - editRepo, notEditableMessage := editFileCommon(ctx, isNewFile) + editRepo := GetEditRepositoryOrFork(ctx, util.Iif(isNewFile, "_new", "_edit")) if editRepo == nil { - // No editable repository, suggest to create a fork - forkToEditFile(ctx, notEditableMessage) return } + editFileCommon(ctx, isNewFile) + canCommit := renderCommitRights(ctx, editRepo) treePath := cleanUploadFileName(ctx.Repo.TreePath) @@ -345,22 +432,8 @@ func NewFile(ctx *context.Context) { editFile(ctx, true) } -// Push branch to user fork, to apply changes on top of -func pushBranchToUserRepo(ctx *context.Context, userRepo *repo_model.Repository, userBranchName string) (err error) { - baseRepo := ctx.Repo.Repository - baseBranchName := ctx.Repo.BranchName - - log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, userRepo.FullName(), userBranchName) - - return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ - Remote: userRepo.RepoPath(), - Branch: baseBranchName + ":" + userBranchName, - Env: repo_module.PushingEnvironment(ctx.Doer, userRepo), - }) -} - func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editRepo, _ := editFileCommon(ctx, isNewFile) + editFileCommon(ctx, isNewFile) ctx.Data["PageHasPosted"] = true @@ -386,19 +459,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } + editRepo := GetEditRepositoryOrError(ctx, tplEditFile, &form) if editRepo == nil { - // No editable repo, maybe the fork was deleted in the meantime - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tplEditFile, &form) return } canCommit := renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if editRepo == ctx.Repo.Repository && branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) + if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplEditFile, &form) { return } @@ -429,20 +498,14 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } - oldBranchName := ctx.Repo.BranchName - if editRepo != ctx.Repo.Repository { - // If editing a user fork, first push the branch to that repository - err := pushBranchToUserRepo(ctx, editRepo, branchName) - if err != nil { - ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tplEditFile, &form) - return - } - oldBranchName = branchName + editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplEditFile, &form) + if err != nil { + return } if _, err := files_service.ChangeRepoFiles(ctx, editRepo, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, - OldBranch: oldBranchName, + OldBranch: editBranchName, NewBranch: branchName, Message: message, Files: []*files_service.ChangeRepoFile{ @@ -534,19 +597,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } } - if editRepo.IsEmpty { - editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) - if err != nil { - log.Error("gitrepo.OpenRepository: %v", err) - } else { - if editGitRepo != nil { - if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") - } - } - editGitRepo.Close() - } - } + UpdateEditRepositoryIsEmpty(ctx, editRepo) redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } @@ -596,6 +647,11 @@ func DiffPreviewPost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { + editRepo := GetEditRepositoryOrFork(ctx, "_delete") + if editRepo == nil { + return + } + ctx.Data["PageIsDelete"] = true ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treePath := cleanUploadFileName(ctx.Repo.TreePath) @@ -606,7 +662,7 @@ func DeleteFile(ctx *context.Context) { } ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) + canCommit := renderCommitRights(ctx, editRepo) ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" @@ -616,7 +672,7 @@ func DeleteFile(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, editRepo) ctx.HTML(http.StatusOK, tplDeleteFile) } @@ -624,7 +680,6 @@ func DeleteFile(ctx *context.Context) { // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName @@ -644,10 +699,15 @@ func DeleteFilePost(ctx *context.Context) { return } - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form) + editRepo := GetEditRepositoryOrError(ctx, tplDeleteFile, &form) + if editRepo == nil { + return + } + + canCommit := renderCommitRights(ctx, editRepo) + + // Cannot commit to an existing branch if user doesn't have rights + if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplDeleteFile, &form) { return } @@ -667,9 +727,14 @@ func DeleteFilePost(ctx *context.Context) { return } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplDeleteFile, &form) + if err != nil { + return + } + + if _, err := files_service.ChangeRepoFiles(ctx, editRepo, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, + OldBranch: editBranchName, NewBranch: branchName, Files: []*files_service.ChangeRepoFile{ { @@ -759,14 +824,19 @@ func DeleteFilePost(ctx *context.Context) { } } - redirectForCommitChoice(ctx, ctx.Repo.Repository, form.CommitChoice, branchName, treePath) + redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, treePath) } // UploadFile render upload file page func UploadFile(ctx *context.Context) { + editRepo := GetEditRepositoryOrFork(ctx, "_upload") + if editRepo == nil { + return + } + ctx.Data["PageIsUpload"] = true upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) + canCommit := renderCommitRights(ctx, editRepo) treePath := cleanUploadFileName(ctx.Repo.TreePath) if treePath != ctx.Repo.TreePath { ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) @@ -790,7 +860,7 @@ func UploadFile(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, editRepo) ctx.HTML(http.StatusOK, tplUploadFile) } @@ -800,11 +870,8 @@ func UploadFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UploadRepoFileForm) ctx.Data["PageIsUpload"] = true upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) - - oldBranchName := ctx.Repo.BranchName - branchName := oldBranchName + branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName } @@ -831,16 +898,14 @@ func UploadFilePost(ctx *context.Context) { return } - if oldBranchName != branchName { - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form) - return - } - } else if !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form) + editRepo := GetEditRepositoryOrError(ctx, tplUploadFile, &form) + if editRepo == nil { + return + } + + canCommit := renderCommitRights(ctx, editRepo) + + if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplUploadFile, &form) { return } @@ -887,9 +952,14 @@ func UploadFilePost(ctx *context.Context) { return } - if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ + editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplUploadFile, &form) + if err != nil { + return + } + + if err := files_service.UploadRepoFiles(ctx, editRepo, ctx.Doer, &files_service.UploadRepoFileOptions{ LastCommitID: ctx.Repo.CommitID, - OldBranch: oldBranchName, + OldBranch: editBranchName, NewBranch: branchName, TreePath: form.TreePath, Message: message, @@ -948,19 +1018,15 @@ func UploadFilePost(ctx *context.Context) { } } else { // os.ErrNotExist - upload file missing in the intervening time?! - log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err) + log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", editRepo, form.TreePath, editBranchName, form.NewBranchName, err) ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) } return } - if ctx.Repo.Repository.IsEmpty { - if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") - } - } + UpdateEditRepositoryIsEmpty(ctx, editRepo) - redirectForCommitChoice(ctx, ctx.Repo.Repository, form.CommitChoice, branchName, form.TreePath) + redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } func cleanUploadFileName(name string) string { @@ -976,6 +1042,7 @@ func cleanUploadFileName(name string) string { } // UploadFileToServer upload file to server file dir not git +// This is independent of any repository, no repository permissions are checked to call this. func UploadFileToServer(ctx *context.Context) { file, header, err := ctx.Req.FormFile("file") if err != nil { @@ -1015,6 +1082,7 @@ func UploadFileToServer(ctx *context.Context) { } // RemoveUploadFileFromServer remove file from server file dir +// This is independent of any repository, no repository permissions are checked to call this. func RemoveUploadFileFromServer(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) if len(form.File) == 0 { diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index da47865116972..a81d3fbbfb9bd 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -24,7 +24,12 @@ const ( // NewDiffPatch render create patch page func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) + editRepo := GetEditRepositoryOrFork(ctx, "_diffpatch") + if editRepo == nil { + return + } + + canCommit := renderCommitRights(ctx, editRepo) ctx.Data["PageIsPatch"] = true @@ -35,7 +40,7 @@ func NewDiffPatch(ctx *context.Context) { } else { ctx.Data["commit_choice"] = frmCommitChoiceNewBranch } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, ctx.Repo.Repository) + ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx, editRepo) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() @@ -47,7 +52,6 @@ func NewDiffPatch(ctx *context.Context) { func NewDiffPatchPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditRepoFileForm) - canCommit := renderCommitRights(ctx, ctx.Repo.Repository) branchName := ctx.Repo.BranchName if form.CommitChoice == frmCommitChoiceNewBranch { branchName = form.NewBranchName @@ -67,11 +71,15 @@ func NewDiffPatchPost(ctx *context.Context) { return } + editRepo := GetEditRepositoryOrError(ctx, tplPatchFile, &form) + if editRepo == nil { + return + } + + canCommit := renderCommitRights(ctx, editRepo) + // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) + if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplPatchFile, &form) { return } @@ -94,9 +102,14 @@ func NewDiffPatchPost(ctx *context.Context) { return } - fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ + editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplPatchFile, &form) + if err != nil { + return + } + + fileResponse, err := files.ApplyDiffPatch(ctx, editRepo, ctx.Doer, &files.ApplyDiffPatchOptions{ LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, + OldBranch: editBranchName, NewBranch: branchName, Message: message, Content: strings.ReplaceAll(form.Content, "\r", ""), @@ -119,7 +132,8 @@ func NewDiffPatchPost(ctx *context.Context) { } if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) + editBranch := editRepo.Owner.Name + "/" + editRepo.Name + ":" + branchName + ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(editBranch)) } else { ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index ee112b83f261b..5ec475b8fec96 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -49,21 +49,6 @@ func MustBeNotEmpty(ctx *context.Context) { } } -// MustBeEditable check that repo can be edited -func MustBeEditable(ctx *context.Context) { - if !ctx.Repo.Repository.CanEnableEditor() { - ctx.NotFound(nil) - return - } -} - -// MustBeAbleToUpload check that repo can be uploaded to -func MustBeAbleToUpload(ctx *context.Context) { - if !setting.Repository.Upload.Enabled { - ctx.NotFound(nil) - } -} - func CommitInfoCache(ctx *context.Context) { var err error ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index b9897ef76b0ac..5a6dee4af732f 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -244,7 +244,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["LineEscapeStatus"] = statuses } if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if ctx.Repo.CanEnableEditor() { if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { ctx.Data["CanEditFile"] = false ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") @@ -254,9 +254,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } else if !ctx.Repo.RefFullName.IsBranch() { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") } } @@ -308,7 +305,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if ctx.Repo.CanEnableEditor() { if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { ctx.Data["CanDeleteFile"] = false ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") @@ -318,7 +315,5 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } else if !ctx.Repo.RefFullName.IsBranch() { ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") } } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 72a8e3e1d1555..32810b3b8bf5b 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -213,7 +213,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil } if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) || !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + if ctx.Repo.CanEnableEditor() { ctx.Data["CanEditReadmeFile"] = true } } diff --git a/routers/web/web.go b/routers/web/web.go index d6ba64f035e5f..aa921e0e81dfc 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1313,29 +1313,29 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo code m.Group("", func() { - m.Group("", func() { - m.Combo("/_edit/*").Get(repo.EditFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) - m.Combo("/_fork_to_edit/*"). - Post(web.Bind(forms.ForkToEditRepoFileForm{}), repo.ForkToEditFilePost) - }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) m.Group("", func() { m.Combo("/_new/*").Get(repo.NewFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) + m.Combo("/_edit/*").Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) m.Combo("/_delete/*").Get(repo.DeleteFile). Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) + m.Combo("/_upload/*", context.MustBeAbleToUpload()).Get(repo.UploadFile). + Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) + m.Combo("/_fork_to_edit/*"). + Post(web.Bind(forms.ForkToEditRepoFileForm{}), repo.ForkToEditFilePost) + }, context.MustEnableEditor()) + m.Group("", func() { m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) - m.Group("", func() { - m.Post("/upload-file", repo.UploadFileToServer) - m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) - }, repo.MustBeAbleToUpload, reqRepoCodeWriter) - }, repo.MustBeEditable, context.RepoMustNotBeArchived()) + }, context.MustBeAbleToCherryPick()) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) + m.Group("", func() { + m.Post("/upload-file", repo.UploadFileToServer) + m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) + }, context.MustBeAbleToUpload()) m.Group("/branches", func() { m.Group("/_new", func() { diff --git a/services/context/permission.go b/services/context/permission.go index 7055f798da3f0..bff08f616d0f2 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -21,10 +21,10 @@ func RequireRepoAdmin() func(ctx *Context) { } } -// CanWriteToBranch checks if the user is allowed to write to the branch of the repo -func CanWriteToBranch() func(ctx *Context) { +// MustBeAbleToCherryPick checks if the user is allowed to cherry-pick to a branch of the repo +func MustBeAbleToCherryPick() func(ctx *Context) { return func(ctx *Context) { - if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) || !ctx.Repo.Repository.CanEnableEditor() { ctx.NotFound(nil) return } diff --git a/services/context/repo.go b/services/context/repo.go index 6f5c772f5e994..68ca875c959da 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -71,9 +71,10 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) } -// CanEnableEditor returns true if repository is editable and user has proper access level. -func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived +// CanEnableEditor returns true if the web editor can be enabled for this repository, +// either by directly writing to the repository or to a user fork. +func (r *Repository) CanEnableEditor() bool { + return r.RefFullName.IsBranch() && r.Repository.CanEnableEditor() } // CanCreateBranch returns true if repository is editable and user has proper access level. @@ -94,10 +95,27 @@ func RepoMustNotBeArchived() func(ctx *Context) { } } +// MustEnableEditor checks if the web editor can be enabled for this repository +func MustEnableEditor() func(ctx *Context) { + return func(ctx *Context) { + if !ctx.Repo.CanEnableEditor() { + ctx.NotFound(nil) + } + } +} + +// MustBeAbleToUpload check that upload is enabled on this site and useful for editing +func MustBeAbleToUpload() func(ctx *Context) { + return func(ctx *Context) { + if !setting.Repository.Upload.Enabled || !ctx.Repo.Repository.CanEnableEditor() { + ctx.NotFound(nil) + } + } +} + // CanCommitToBranchResults represents the results of CanCommitToBranch type CanCommitToBranchResults struct { CanCommitToBranch bool - EditorEnabled bool UserCanPush bool RequireSigned bool WillSign bool @@ -123,7 +141,7 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) - canCommit := r.CanEnableEditor(ctx, doer) && userCanPush + canCommit := r.CanEnableEditor() && r.CanWriteToBranch(ctx, doer, r.BranchName) && userCanPush if requireSigned { canCommit = canCommit && sign } @@ -139,7 +157,6 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use return CanCommitToBranchResults{ CanCommitToBranch: canCommit, - EditorEnabled: r.CanEnableEditor(ctx, doer), UserCanPush: userCanPush, RequireSigned: requireSigned, WillSign: sign, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 306c680ccf966..b5d72c7fb299f 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -735,7 +735,8 @@ func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) b // ForkToEditRepoFileForm form for forking the repo to edit a file type ForkToEditRepoFileForm struct { - TreePath string `binding:"Required;MaxSize(500)"` + TreePath string `binding:"Required;MaxSize(500)"` + EditOperation string `binding:"Required;MaxSize(20)"` } // Validate validates the fields diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index 2c0c2fc792e07..4e532e94a0970 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -5,6 +5,11 @@ {{template "base/alert" .}} <form class="ui form" method="post"> {{.CsrfTokenHtml}} + {{if .ForkRepo}} + <div class="ui blue message"> + <p>{{ctx.Locale.Tr "repo.editor.fork_edit_description" .ForkRepo.FullName}}</p> + </div> + {{end}} <input type="hidden" name="last_commit" value="{{.last_commit}}"> {{template "repo/editor/commit_form" .}} </form> diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl index f25d303d15789..f89bf2f3f2f72 100644 --- a/templates/repo/editor/fork.tmpl +++ b/templates/repo/editor/fork.tmpl @@ -20,6 +20,7 @@ {{end}} {{end}} <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> + <input type="hidden" id="edit_operation" name="edit_operation" value="{{.EditOperation}}" required> </div> </div> <div class="field center"> diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index 33a7c2a89d8c7..58160e30e5aa9 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -10,6 +10,11 @@ {{.CsrfTokenHtml}} <input type="hidden" name="last_commit" value="{{.last_commit}}"> <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{if .ForkRepo}} + <div class="ui blue message"> + <p>{{ctx.Locale.Tr "repo.editor.fork_edit_description" .ForkRepo.FullName}}</p> + </div> + {{end}} <div class="repo-editor-header"> <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> {{ctx.Locale.Tr "repo.editor.patching"}} diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 572502040678a..2b3737eeac32d 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -5,6 +5,11 @@ {{template "base/alert" .}} <form class="ui comment form" method="post"> {{.CsrfTokenHtml}} + {{if .ForkRepo}} + <div class="ui blue message"> + <p>{{ctx.Locale.Tr "repo.editor.fork_edit_description" .ForkRepo.FullName}}</p> + </div> + {{end}} <div class="repo-editor-header"> <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index ae3f95045bc58..c1975a73a624b 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -26,7 +26,7 @@ <h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3> <div class="repo-button-row"> - {{if and .CanWriteCode (not .Repository.IsArchived)}} + {{if and .CanWriteCode .Repository.CanEnableEditor}} <a class="ui small button" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/"> {{ctx.Locale.Tr "repo.editor.new_file"}} </a> diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 292a2f878c800..65d1c5d2846e7 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,7 +41,7 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} + {{if and .RefFullName.IsBranch (not .IsViewFile)}} <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} From c74b5fc1f26898cac6d16531407a5d2ba77fa181 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 23 Apr 2025 04:53:50 +0200 Subject: [PATCH 04/22] Check for protected branch also when committing to new branch --- routers/api/v1/api.go | 2 +- routers/api/v1/repo/file.go | 2 +- routers/web/repo/editor.go | 33 +++++++++++++++++++-------------- routers/web/repo/patch.go | 4 ++-- services/context/permission.go | 2 +- services/context/repo.go | 20 ++++++++++++-------- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b9b590725b42b..5801a5730749f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -434,7 +434,7 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { // reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin func reqRepoBranchWriter(ctx *context.APIContext) { options, ok := web.GetForm(ctx).(api.FileOptionInterface) - if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { + if !ok || (!context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, options.Branch()) && !ctx.IsUserSiteAdmin()) { ctx.APIError(http.StatusForbidden, "user should have a permission to write to this branch") return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index d8e9fde2c006b..fab0a839cf048 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -405,7 +405,7 @@ func GetEditorconfig(ctx *context.APIContext) { // canWriteFiles returns true if repository is editable and user has proper access level. func canWriteFiles(ctx *context.APIContext, branch string) bool { - return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) && + return context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, branch) && !ctx.Repo.Repository.IsMirror && !ctx.Repo.Repository.IsArchived } diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index f9d6a2de97699..805d17d434dbd 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -53,7 +53,7 @@ func canCreateBasePullRequest(ctx *context.Context, editRepo *repo_model.Reposit func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) bool { if editRepo == ctx.Repo.Repository { // Editing the same repository that we are viewing - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) + canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { log.Error("CanCommitToBranch: %v", err) } @@ -64,7 +64,7 @@ func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) b return canCommitToBranch.CanCommitToBranch } - // Editing a user fork of the repository we are viewing + // Editing a user fork of the repository we are viewing, always choose a new branch ctx.Data["CanCommitToBranch"] = context.CanCommitToBranchResults{} ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) @@ -121,7 +121,7 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { // This may be a fork of the repository owned by the user. If no repository can be found // for editing, nil is returned along with a message explaining why editing is not possible. func getEditRepository(ctx *context.Context) (*repo_model.Repository, any) { - if ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + if context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) { return ctx.Repo.Repository, nil } @@ -197,18 +197,23 @@ func GetEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form // CheckPushEditBranch chesk if pushing to the branch in the edit repository is possible, // and if not renders an error and returns false. -func CheckCanPushEditBranch(ctx *context.Context, editRepo *repo_model.Repository, branchName string, canCommit bool, tpl templates.TplName, form any) bool { +func CheckCanPushEditBranch(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) bool { + // When pushing to a fork or another branch on the same repository, it should not exist yet if ctx.Repo.Repository != editRepo || ctx.Repo.BranchName != branchName { - // When pushing to a fork or another branch on the same repository, it should not exist yet if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) return false } - } else if ctx.Repo.Repository == editRepo && !canCommit { - // When we can't commit to the same branch on the same repository, it's protected + } + + // Check for protected branch + canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, editRepo, branchName) + if err != nil { + log.Error("CanCommitToBranch: %v", err) + } + if !canCommitToBranch.CanCommitToBranch { ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tpl, form) return false } @@ -464,10 +469,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } - canCommit := renderCommitRights(ctx, editRepo) + renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplEditFile, &form) { + if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplEditFile, &form) { return } @@ -704,10 +709,10 @@ func DeleteFilePost(ctx *context.Context) { return } - canCommit := renderCommitRights(ctx, editRepo) + renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplDeleteFile, &form) { + if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplDeleteFile, &form) { return } @@ -903,9 +908,9 @@ func UploadFilePost(ctx *context.Context) { return } - canCommit := renderCommitRights(ctx, editRepo) + renderCommitRights(ctx, editRepo) - if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplUploadFile, &form) { + if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplUploadFile, &form) { return } diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index a81d3fbbfb9bd..dbb252a48cbf7 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -76,10 +76,10 @@ func NewDiffPatchPost(ctx *context.Context) { return } - canCommit := renderCommitRights(ctx, editRepo) + renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, canCommit, tplPatchFile, &form) { + if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplPatchFile, &form) { return } diff --git a/services/context/permission.go b/services/context/permission.go index bff08f616d0f2..883db5e649258 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -24,7 +24,7 @@ func RequireRepoAdmin() func(ctx *Context) { // MustBeAbleToCherryPick checks if the user is allowed to cherry-pick to a branch of the repo func MustBeAbleToCherryPick() func(ctx *Context) { return func(ctx *Context) { - if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) || !ctx.Repo.Repository.CanEnableEditor() { + if !CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) || !ctx.Repo.Repository.CanEnableEditor() { ctx.NotFound(nil) return } diff --git a/services/context/repo.go b/services/context/repo.go index 68ca875c959da..a7add1906fc95 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -67,8 +67,13 @@ type Repository struct { } // CanWriteToBranch checks if the branch is writable by the user -func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User, branch string) bool { - return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) +func CanWriteToBranch(ctx context.Context, user *user_model.User, repo *repo_model.Repository, branch string) bool { + permission, err := access_model.GetUserRepoPermission(ctx, repo, user) + if err != nil { + return false + } + + return issues_model.CanMaintainerWriteToBranch(ctx, permission, branch, user) } // CanEnableEditor returns true if the web editor can be enabled for this repository, @@ -124,24 +129,23 @@ type CanCommitToBranchResults struct { } // CanCommitToBranch returns true if repository is editable and user has proper access level -// // and branch is not protected for push -func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) { - protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) +func CanCommitToBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branchName string) (CanCommitToBranchResults, error) { + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) if err != nil { return CanCommitToBranchResults{}, err } userCanPush := true requireSigned := false if protectedBranch != nil { - protectedBranch.Repo = r.Repository + protectedBranch.Repo = repo userCanPush = protectedBranch.CanUserPush(ctx, doer) requireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) + sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), git.BranchPrefix+branchName) - canCommit := r.CanEnableEditor() && r.CanWriteToBranch(ctx, doer, r.BranchName) && userCanPush + canCommit := repo.CanEnableEditor() && CanWriteToBranch(ctx, doer, repo, branchName) && userCanPush if requireSigned { canCommit = canCommit && sign } From 64fa570c4ba4284e2da3986542384787619aac68 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 23 Apr 2025 04:04:01 +0200 Subject: [PATCH 05/22] Add tests --- tests/integration/editor_test.go | 305 +++++++++++++++++++----- tests/integration/pull_compare_test.go | 2 +- tests/integration/pull_merge_test.go | 24 +- tests/integration/pull_review_test.go | 2 +- tests/integration/pull_status_test.go | 6 +- tests/integration/repo_activity_test.go | 4 +- tests/integration/repo_webhook_test.go | 4 +- 7 files changed, 269 insertions(+), 78 deletions(-) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index a5936d86de225..112fc6ded1fd9 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -30,13 +30,21 @@ import ( func TestCreateFile(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test.txt", "Content") }) } -func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder { +func TestCreateFileFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + forkToEdit(t, session, "user2", "repo1", "_new", "master", "test.txt") + testCreateFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "test.txt", "Content") + }) +} + +func testCreateFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath, content string) { // Request editor page - newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch) + newURL := fmt.Sprintf("/%s/%s/_new/%s/", owner, repo, branch) req := NewRequest(t, "GET", newURL) resp := session.MakeRequest(t, req, http.StatusOK) @@ -46,70 +54,87 @@ func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, file // Save new file to master branch req = NewRequestWithValues(t, "POST", newURL, map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": content, - "commit_choice": "direct", + "_csrf": doc.GetCSRF(), + "last_commit": lastCommit, + "tree_path": filePath, + "content": content, + "commit_choice": commitChoice, + "new_branch_name": targetBranch, }) - return session.MakeRequest(t, req, http.StatusSeeOther) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check new file exists + req = NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", user, repo, targetBranch, filePath) + session.MakeRequest(t, req, http.StatusOK) } func TestCreateFileOnProtectedBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") + testCreateFileOnProtectedBranch(t, session, "user2", "user2", "repo1", "master", "master", "direct") + }) +} - csrf := GetUserCSRFToken(t, session) - // Change master branch to protected - req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ - "_csrf": csrf, - "rule_name": "master", - "enable_push": "true", - }) - session.MakeRequest(t, req, http.StatusSeeOther) - // Check if master branch has been locked successfully - flashMsg := session.GetCookieFlashMessage() - assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) - - // Request editor page - req = NewRequest(t, "GET", "/user2/repo1/_new/master/") - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Save new file to master branch - req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": "test.txt", - "content": "Content", - "commit_choice": "direct", - }) +func TestCreateFileOnProtectedBranchFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + forkToEdit(t, session, "user2", "repo1", "_new", "master", "test.txt") + testCreateFileOnProtectedBranch(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch") + }) +} - resp = session.MakeRequest(t, req, http.StatusOK) - // Check body for error message - assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".") +func testCreateFileOnProtectedBranch(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice string) { + csrf := GetUserCSRFToken(t, session) + // Change target branch to protected + req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings", "branches", "edit"), map[string]string{ + "_csrf": csrf, + "rule_name": targetBranch, + "enable_push": "true", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + // Check if target branch has been locked successfully + flashMsg := session.GetCookieFlashMessage() + assert.Equal(t, fmt.Sprintf(`Branch protection for rule "%s" has been updated.`, targetBranch), flashMsg.SuccessMsg) - // remove the protected branch - csrf = GetUserCSRFToken(t, session) + // Request editor page + req = NewRequest(t, "GET", path.Join(owner, repo, "_new", branch)) + resp := session.MakeRequest(t, req, http.StatusOK) - // Change master branch to protected - req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{ - "_csrf": csrf, - }) + doc := NewHTMLParser(t, resp.Body) + lastCommit := doc.GetInputValueByName("last_commit") + assert.NotEmpty(t, lastCommit) - resp = session.MakeRequest(t, req, http.StatusOK) + // Save new file to target branch + req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_new", branch), map[string]string{ + "_csrf": doc.GetCSRF(), + "last_commit": lastCommit, + "tree_path": "test.txt", + "content": "Content", + "commit_choice": commitChoice, + "new_branch_name": targetBranch, + }) - res := make(map[string]string) - assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res)) - assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"]) + resp = session.MakeRequest(t, req, http.StatusOK) + // Check body for error message + assert.Contains(t, resp.Body.String(), fmt.Sprintf("Cannot commit to protected branch "%s".", targetBranch)) + + // remove the protected branch + csrf = GetUserCSRFToken(t, session) - // Check if master branch has been locked successfully - flashMsg = session.GetCookieFlashMessage() - assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg) + // Change target branch to protected + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings", "branches", "1", "delete"), map[string]string{ + "_csrf": csrf, }) + + resp = session.MakeRequest(t, req, http.StatusOK) + + res := make(map[string]string) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res)) + assert.Equal(t, "/"+path.Join(user, repo, "settings", "branches"), res["redirect"]) + + // Check if target branch has been locked successfully + flashMsg = session.GetCookieFlashMessage() + assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg) } func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder { @@ -141,9 +166,9 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa return resp } -func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder { +func testEditFileToNewBranch(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder { // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) + req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -151,7 +176,7 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra assert.NotEmpty(t, lastCommit) // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), + req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_edit", branch, filePath), map[string]string{ "_csrf": htmlDoc.GetCSRF(), "last_commit": lastCommit, @@ -181,10 +206,110 @@ func TestEditFile(t *testing.T) { func TestEditFileToNewBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") + testEditFileToNewBranch(t, session, "user2", "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") + }) +} + +func TestEditFileToNewBranchFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + forkToEdit(t, session, "user2", "repo1", "_edit", "master", "README.md") + testEditFileToNewBranch(t, session, "user4", "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") + }) +} + +func TestDeleteFile(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") + testDeleteFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "README.md") + }) +} + +func TestDeleteFileFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + forkToEdit(t, session, "user2", "repo1", "_delete", "master", "README.md") + testDeleteFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "README.md") }) } +func testDeleteFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath string) { + // Check file exists + req := NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", owner, repo, branch, filePath) + session.MakeRequest(t, req, http.StatusOK) + + // Request editor page + newURL := fmt.Sprintf("/%s/%s/_delete/%s/%s", owner, repo, branch, filePath) + req = NewRequest(t, "GET", newURL) + resp := session.MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + lastCommit := doc.GetInputValueByName("last_commit") + assert.NotEmpty(t, lastCommit) + + // Save new file to master branch + req = NewRequestWithValues(t, "POST", newURL, map[string]string{ + "_csrf": doc.GetCSRF(), + "last_commit": lastCommit, + "tree_path": filePath, + "commit_choice": commitChoice, + "new_branch_name": targetBranch, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check file was deleted + req = NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", user, repo, targetBranch, filePath) + session.MakeRequest(t, req, http.StatusNotFound) +} + +func TestPatchFile(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") + testPatchFile(t, session, "user2", "user2", "repo1", "master", "feature/test") + }) +} + +func TestPatchFileFork(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + forkToEdit(t, session, "user2", "repo1", "_diffpatch", "master", "README.md") + testPatchFile(t, session, "user4", "user2", "repo1", "master", "feature/test") + }) +} + +func testPatchFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch string) { + // Request editor page + newURL := fmt.Sprintf("/%s/%s/_diffpatch/%s/", owner, repo, branch) + req := NewRequest(t, "GET", newURL) + resp := session.MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + lastCommit := doc.GetInputValueByName("last_commit") + assert.NotEmpty(t, lastCommit) + + // Save new file to master branch + req = NewRequestWithValues(t, "POST", newURL, map[string]string{ + "_csrf": doc.GetCSRF(), + "last_commit": lastCommit, + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++File 1 +`, + "commit_choice": "commit-to-new-branch", + "new_branch_name": targetBranch, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check new file exists + req = NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", user, repo, targetBranch, "patch-file-1.txt") + session.MakeRequest(t, req, http.StatusOK) +} + func TestWebGitCommitEmail(t *testing.T) { onGiteaRun(t, func(t *testing.T, _ *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -337,3 +462,69 @@ index 0000000000..bbbbbbbbbb }) }) } + +func forkToEdit(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) { + // Attempt to edit file + req := NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Fork + req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork_to_edit", branch), + map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "tree_path": filePath, + "edit_operation": operation, + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) +} + +func testForkToEdit(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { + // Fork repository because we can't edit it + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + + // Check the existence of the forked repo + req := NewRequestf(t, "GET", "/%s/%s/settings", user, repo) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Archive the repository + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "repo_name": repo, + "action": "archive", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check editing archived repository is disabled + req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Fork Repository Not Editable") + + // Unfork the repository + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "repo_name": repo, + "action": "convert_fork", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Fork repository again + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + + // Check the existence of the forked repo with unique name + req = NewRequestf(t, "GET", "/%s/%s-1", user, repo) + session.MakeRequest(t, req, http.StatusOK) +} + +func TestForkToEdit(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + testForkToEdit(t, session, "user4", "user2", "repo1", "master", "README.md") + }) +} diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index 86bdd1b9e351c..c29d2001ad7f0 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -104,7 +104,7 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { assert.True(t, forkedRepo.IsPrivate) // user4 creates a new branch and a PR - testEditFileToNewBranch(t, user4Session, "user4", forkedRepoName, "master", "user4/update-readme", "README.md", "Hello, World\n(Edited by user4)\n") + testEditFileToNewBranch(t, user4Session, "user4", "user4", forkedRepoName, "master", "user4/update-readme", "README.md", "Hello, World\n(Edited by user4)\n") resp := testPullCreateDirectly(t, user4Session, repo3.OwnerName, repo3.Name, "master", "user4", forkedRepoName, "user4/update-readme", "PR for user4 forked repo3") prURL := test.RedirectURL(resp) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index cf50d5e639e2d..3080f484ce2da 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -180,7 +180,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n") resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title") @@ -234,8 +234,8 @@ func TestCantMergeConflict(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") // Use API to create a conflicting pr token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) @@ -280,7 +280,7 @@ func TestCantMergeUnrelated(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") // Now we want to create a commit on a branch that is totally unrelated to our current head // Drop down to pure code at this point @@ -343,7 +343,7 @@ func TestCantMergeUnrelated(t *testing.T) { _, _, err = git.NewCommand("branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(git.DefaultContext, &git.RunOpts{Dir: path}) assert.NoError(t, err) - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") // Use API to create a conflicting pr token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) @@ -375,7 +375,7 @@ func TestFastForwardOnlyMerge(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n") // Use API to create a pr from update to master token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) @@ -416,7 +416,7 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n") // Use API to create a pr from diverging to update @@ -538,9 +538,9 @@ func TestConflictChecking(t *testing.T) { func TestPullRetargetChildOnBranchDelete(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") - testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n") + testEditFileToNewBranch(t, session, "user2", "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)") respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request") elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/") @@ -573,8 +573,8 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n") - testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)") respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request") elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/") @@ -610,7 +610,7 @@ func TestPullRequestMergedWithNoPermissionDeleteBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user4") testRepoFork(t, session, "user2", "repo1", "user4", "repo1", "") - testEditFileToNewBranch(t, session, "user4", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n") + testEditFileToNewBranch(t, session, "user4", "user4", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n") respBasePR := testPullCreate(t, session, "user4", "repo1", false, "master", "base-pr", "Base Pull Request") elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/") diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 13b2384e9c95b..2fd1b407b61b3 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -246,7 +246,7 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) { t.Run("Submit approve/reject review on closed PR", func(t *testing.T) { // Created a closed PR (made by user1) in the upstream repo1. - testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n") + testEditFileToNewBranch(t, user1Session, "user1", "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n") resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title") elem := strings.Split(test.RedirectURL(resp), "/") assert.Equal(t, "pulls", elem[3]) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 63ffe9432035f..715d41bb3cb1b 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -24,7 +24,7 @@ func TestPullCreate_CommitStatus(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "status1", "README.md", "status1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, @@ -123,8 +123,8 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") - testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "status1", "README.md", "status1") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go index d5025decba078..85110e064c32c 100644 --- a/tests/integration/repo_activity_test.go +++ b/tests/integration/repo_activity_test.go @@ -29,10 +29,10 @@ func TestRepoActivity(t *testing.T) { assert.Equal(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n") testPullCreate(t, session, "user1", "repo1", false, "master", "feat/better_readme", "This is a pull title") - testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n") + testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n") testPullCreate(t, session, "user1", "repo1", false, "master", "feat/much_better_readme", "This is a pull title") // Create issues (3 new issues) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 89df15b8de8c3..bb4747ccd0e26 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -310,7 +310,7 @@ func Test_WebhookPush(t *testing.T) { testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push") // 2. trigger the webhook - testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push") // 3. validate the webhook is triggered assert.Equal(t, "push", triggeredEvent) @@ -602,7 +602,7 @@ func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only") // 2. trigger the webhook with a push action - testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push") // 3. validate the webhook is triggered with right event assert.Equal(t, "push", trigger) From 4da881b7a350512ac4b8ed62020942b53127aba1 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 23 Apr 2025 13:05:22 +0200 Subject: [PATCH 06/22] Fix wrong redirect URL after fork, add test --- routers/web/repo/editor.go | 4 ++-- tests/integration/editor_test.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 805d17d434dbd..1005dca945c63 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -178,7 +178,7 @@ func GetEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_m } // No editable repository, suggest to create a fork - forkToEditFileCommon(ctx, ctx.Repo.TreePath, editOperation, notEditableMessage) + forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) ctx.HTML(http.StatusOK, tplForkFile) return nil } @@ -299,7 +299,7 @@ func ForkToEditFilePost(ctx *context.Context) { SingleBranch: ctx.Repo.BranchName, }, tplForkFile, form) if err != nil { - forkToEditFileCommon(ctx, form.TreePath, form.EditOperation, notEditableMessage) + forkToEditFileCommon(ctx, form.EditOperation, form.TreePath, notEditableMessage) ctx.HTML(http.StatusOK, tplForkFile) return } diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index 112fc6ded1fd9..b47f4cb7c43ba 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -20,6 +20,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -477,7 +478,8 @@ func forkToEdit(t *testing.T, session *TestSession, owner, repo, operation, bran "edit_operation": operation, }, ) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/"+path.Join(owner, repo, operation, branch, filePath), test.RedirectURL(resp)) } func testForkToEdit(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { From f29e486f36e931c33a43f23127daa93a5ec49c86 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 23 Apr 2025 13:16:49 +0200 Subject: [PATCH 07/22] Move fork to edit logic to a separate file --- routers/web/repo/editor.go | 245 ++----------------------------- routers/web/repo/fork.go | 10 +- routers/web/repo/fork_to_edit.go | 240 ++++++++++++++++++++++++++++++ routers/web/repo/patch.go | 8 +- 4 files changed, 263 insertions(+), 240 deletions(-) create mode 100644 routers/web/repo/fork_to_edit.go diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 1005dca945c63..0274bcf677c9a 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -11,16 +11,13 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" @@ -30,7 +27,6 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" - repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -39,7 +35,6 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" - tplForkFile templates.TplName = "repo/editor/fork" frmCommitChoiceDirect string = "direct" frmCommitChoiceNewBranch string = "commit-to-new-branch" @@ -117,201 +112,6 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } -// getEditRepository returns the repository where the actual edits will be written to. -// This may be a fork of the repository owned by the user. If no repository can be found -// for editing, nil is returned along with a message explaining why editing is not possible. -func getEditRepository(ctx *context.Context) (*repo_model.Repository, any) { - if context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) { - return ctx.Repo.Repository, nil - } - - // If we can't write to the branch, try find a user fork to create a branch in instead - userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) - if err != nil { - log.Error("GetUserFork: %v", err) - return nil, nil - } - if userRepo == nil { - return nil, nil - } - - // Load repository information - if err := userRepo.LoadOwner(ctx); err != nil { - log.Error("LoadOwner: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } - if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { - if err != nil { - log.Error("GetBaseRepo: %v", err) - } else { - log.Error("GetBaseRepo: Expected a base repo for user fork", err) - } - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } - - // Check code unit, archiving and permissions. - if !userRepo.UnitEnabled(ctx, unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) - } - if userRepo.IsArchived { - return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) - } - permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) - if err != nil { - log.Error("access_model.GetUserRepoPermission: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) - } - if !permission.CanWrite(unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) - } - - ctx.Data["ForkRepo"] = userRepo - return userRepo, nil -} - -// GetEditRepository returns the repository where the edits will be written to. -// If no repository is editable, redirects to a page to create a fork. -func GetEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_model.Repository { - editRepo, notEditableMessage := getEditRepository(ctx) - if editRepo != nil { - return editRepo - } - - // No editable repository, suggest to create a fork - forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) - ctx.HTML(http.StatusOK, tplForkFile) - return nil -} - -// GetEditRepository returns the repository where the edits will be written to. -// If no repository is editable, display an error. -func GetEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form any) *repo_model.Repository { - editRepo, _ := getEditRepository(ctx) - if editRepo == nil { - // No editable repo, maybe the fork was deleted in the meantime - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tpl, form) - return nil - } - return editRepo -} - -// CheckPushEditBranch chesk if pushing to the branch in the edit repository is possible, -// and if not renders an error and returns false. -func CheckCanPushEditBranch(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) bool { - // When pushing to a fork or another branch on the same repository, it should not exist yet - if ctx.Repo.Repository != editRepo || ctx.Repo.BranchName != branchName { - if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) - return false - } - } - - // Check for protected branch - canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, editRepo, branchName) - if err != nil { - log.Error("CanCommitToBranch: %v", err) - } - if !canCommitToBranch.CanCommitToBranch { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tpl, form) - return false - } - - return true -} - -// PushEditBranchOrError pushes the branch that editing will be applied on top of -// to the user fork, if needed. On failure, it displays and returns an error. The -// branch name to be used for editing is returned. -func PushEditBranchOrError(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) (string, error) { - if editRepo == ctx.Repo.Repository { - return ctx.Repo.BranchName, nil - } - - // If editing a user fork, first push the branch to that repository - baseRepo := ctx.Repo.Repository - baseBranchName := ctx.Repo.BranchName - - log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, editRepo.FullName(), branchName) - - if err := git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ - Remote: editRepo.RepoPath(), - Branch: baseBranchName + ":" + branchName, - Env: repo_module.PushingEnvironment(ctx.Doer, editRepo), - }); err != nil { - ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tpl, form) - return "", nil - } - - return branchName, nil -} - -// UpdateEditRepositoryIsEmpty updates the the edit repository to mark it as no longer empty -func UpdateEditRepositoryIsEmpty(ctx *context.Context, editRepo *repo_model.Repository) { - if !editRepo.IsEmpty { - return - } - - editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) - if err != nil { - log.Error("gitrepo.OpenRepository: %v", err) - return - } - if editGitRepo == nil { - return - } - - if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") - } - editGitRepo.Close() -} - -func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, _ := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) - - ctx.Data["EditOperation"] = editOperation - ctx.Data["TreePath"] = treePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["CanForkRepo"] = notEditableMessage == nil - ctx.Data["NotEditableMessage"] = notEditableMessage -} - -func ForkToEditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) - - editRepo, notEditableMessage := getEditRepository(ctx) - - ctx.Data["PageHasPosted"] = true - - // Fork repository, if it doesn't already exist - if editRepo == nil && notEditableMessage == nil { - _, err := ForkRepository(ctx, ctx.Doer, repo_service.ForkRepoOptions{ - BaseRepo: ctx.Repo.Repository, - Name: GetUniqueRepositoryName(ctx, ctx.Repo.Repository.Name), - Description: ctx.Repo.Repository.Description, - SingleBranch: ctx.Repo.BranchName, - }, tplForkFile, form) - if err != nil { - forkToEditFileCommon(ctx, form.EditOperation, form.TreePath, notEditableMessage) - ctx.HTML(http.StatusOK, tplForkFile) - return - } - } - - // Redirect back to editing page - ctx.Redirect(path.Join(ctx.Repo.RepoLink, form.EditOperation, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(form.TreePath))) -} - -// editFileCommon setup common context, and return repository that will be edited. -// If no repository can be found for editing, nil is returned along with a message -// explaining why editing is not possible. func editFileCommon(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile @@ -323,7 +123,7 @@ func editFileCommon(ctx *context.Context, isNewFile bool) { } func editFile(ctx *context.Context, isNewFile bool) { - editRepo := GetEditRepositoryOrFork(ctx, util.Iif(isNewFile, "_new", "_edit")) + editRepo := getEditRepositoryOrFork(ctx, util.Iif(isNewFile, "_new", "_edit")) if editRepo == nil { return } @@ -464,7 +264,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } - editRepo := GetEditRepositoryOrError(ctx, tplEditFile, &form) + editRepo := getEditRepositoryOrError(ctx, tplEditFile, &form) if editRepo == nil { return } @@ -472,7 +272,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplEditFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, tplEditFile, &form) { return } @@ -503,7 +303,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } - editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplEditFile, &form) + editBranchName, err := pushToEditRepositoryOrError(ctx, editRepo, branchName, tplEditFile, &form) if err != nil { return } @@ -602,7 +402,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } } - UpdateEditRepositoryIsEmpty(ctx, editRepo) + updateEditRepositoryIsEmpty(ctx, editRepo) redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } @@ -652,7 +452,7 @@ func DiffPreviewPost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - editRepo := GetEditRepositoryOrFork(ctx, "_delete") + editRepo := getEditRepositoryOrFork(ctx, "_delete") if editRepo == nil { return } @@ -704,7 +504,7 @@ func DeleteFilePost(ctx *context.Context) { return } - editRepo := GetEditRepositoryOrError(ctx, tplDeleteFile, &form) + editRepo := getEditRepositoryOrError(ctx, tplDeleteFile, &form) if editRepo == nil { return } @@ -712,7 +512,7 @@ func DeleteFilePost(ctx *context.Context) { renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplDeleteFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, tplDeleteFile, &form) { return } @@ -732,7 +532,7 @@ func DeleteFilePost(ctx *context.Context) { return } - editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplDeleteFile, &form) + editBranchName, err := pushToEditRepositoryOrError(ctx, editRepo, branchName, tplDeleteFile, &form) if err != nil { return } @@ -834,7 +634,7 @@ func DeleteFilePost(ctx *context.Context) { // UploadFile render upload file page func UploadFile(ctx *context.Context) { - editRepo := GetEditRepositoryOrFork(ctx, "_upload") + editRepo := getEditRepositoryOrFork(ctx, "_upload") if editRepo == nil { return } @@ -903,14 +703,14 @@ func UploadFilePost(ctx *context.Context) { return } - editRepo := GetEditRepositoryOrError(ctx, tplUploadFile, &form) + editRepo := getEditRepositoryOrError(ctx, tplUploadFile, &form) if editRepo == nil { return } renderCommitRights(ctx, editRepo) - if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplUploadFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, tplUploadFile, &form) { return } @@ -957,7 +757,7 @@ func UploadFilePost(ctx *context.Context) { return } - editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplUploadFile, &form) + editBranchName, err := pushToEditRepositoryOrError(ctx, editRepo, branchName, tplUploadFile, &form) if err != nil { return } @@ -1029,7 +829,7 @@ func UploadFilePost(ctx *context.Context) { return } - UpdateEditRepositoryIsEmpty(ctx, editRepo) + updateEditRepositoryIsEmpty(ctx, editRepo) redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } @@ -1122,23 +922,6 @@ func GetUniquePatchBranchName(ctx *context.Context, editRepo *repo_model.Reposit return "" } -// GetUniqueRepositoryName Gets a unique repository name for a user -// It will append a -<num> postfix if the name is already taken -func GetUniqueRepositoryName(ctx *context.Context, name string) string { - uniqueName := name - i := 1 - - for { - _, err := repo_model.GetRepositoryByName(ctx, ctx.Doer.ID, uniqueName) - if err != nil || repo_model.IsErrRepoNotExist(err) { - return uniqueName - } - - uniqueName = fmt.Sprintf("%s-%d", name, i) - i++ - } -} - // GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is // deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a // SubTree or it has no entries, we go up one dir and see if we can return the user to that listing. diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 2928c4650124d..fcb883d0f7a7d 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -194,20 +194,20 @@ func ForkPost(ctx *context.Context) { } } - repo, err := ForkRepository(ctx, ctxUser, repo_service.ForkRepoOptions{ + repo := forkRepositoryOrError(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, }, tplFork, form) - if err != nil { + if repo == nil { return } ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) } -func ForkRepository(ctx *context.Context, user *user_model.User, opts repo_service.ForkRepoOptions, tpl templates.TplName, form any) (*repo_model.Repository, error) { +func forkRepositoryOrError(ctx *context.Context, user *user_model.User, opts repo_service.ForkRepoOptions, tpl templates.TplName, form any) *repo_model.Repository { repo, err := repo_service.ForkRepository(ctx, ctx.Doer, user, opts) if err != nil { ctx.Data["Err_RepoName"] = true @@ -238,9 +238,9 @@ func ForkRepository(ctx *context.Context, user *user_model.User, opts repo_servi default: ctx.ServerError("ForkPost", err) } - return repo, err + return repo } log.Trace("Repository forked[%d]: %s/%s", opts.BaseRepo.ID, user.Name, repo.Name) - return repo, err + return repo } diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go new file mode 100644 index 0000000000000..b17454736c172 --- /dev/null +++ b/routers/web/repo/fork_to_edit.go @@ -0,0 +1,240 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "fmt" + "net/http" + "path" + "strings" + + git_model "code.gitea.io/gitea/models/git" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" + "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 ( + tplForkFile templates.TplName = "repo/editor/fork" +) + +// getEditRepository returns the repository where the actual edits will be written to. +// This may be a fork of the repository owned by the user. If no repository can be found +// for editing, nil is returned along with a message explaining why editing is not possible. +func getEditRepository(ctx *context.Context) (*repo_model.Repository, any) { + if context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) { + return ctx.Repo.Repository, nil + } + + // If we can't write to the branch, try find a user fork to create a branch in instead + userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + if err != nil { + log.Error("GetUserFork: %v", err) + return nil, nil + } + if userRepo == nil { + return nil, nil + } + + // Load repository information + if err := userRepo.LoadOwner(ctx); err != nil { + log.Error("LoadOwner: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { + if err != nil { + log.Error("GetBaseRepo: %v", err) + } else { + log.Error("GetBaseRepo: Expected a base repo for user fork", err) + } + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + + // Check code unit, archiving and permissions. + if !userRepo.UnitEnabled(ctx, unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) + } + if userRepo.IsArchived { + return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) + } + permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) + if err != nil { + log.Error("access_model.GetUserRepoPermission: %v", err) + return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + } + if !permission.CanWrite(unit.TypeCode) { + return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) + } + + ctx.Data["ForkRepo"] = userRepo + return userRepo, nil +} + +// GetEditRepository returns the repository where the edits will be written to. +// If no repository is editable, redirects to a page to create a fork. +func getEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_model.Repository { + editRepo, notEditableMessage := getEditRepository(ctx) + if editRepo != nil { + return editRepo + } + + // No editable repository, suggest to create a fork + forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) + return nil +} + +// GetEditRepository returns the repository where the edits will be written to. +// If no repository is editable, display an error. +func getEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form any) *repo_model.Repository { + editRepo, _ := getEditRepository(ctx) + if editRepo == nil { + // No editable repo, maybe the fork was deleted in the meantime + ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tpl, form) + return nil + } + return editRepo +} + +// CheckPushEditBranch chesk if pushing to the branch in the edit repository is possible, +// and if not renders an error and returns false. +func canPushToEditRepository(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) bool { + // When pushing to a fork or another branch on the same repository, it should not exist yet + if ctx.Repo.Repository != editRepo || ctx.Repo.BranchName != branchName { + if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { + ctx.Data["Err_NewBranchName"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) + return false + } + } + + // Check for protected branch + canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, editRepo, branchName) + if err != nil { + log.Error("CanCommitToBranch: %v", err) + } + if !canCommitToBranch.CanCommitToBranch { + ctx.Data["Err_NewBranchName"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tpl, form) + return false + } + + return true +} + +// pushToEditRepositoryOrError pushes the branch that editing will be applied on top of +// to the user fork, if needed. On failure, it displays and returns an error. The +// branch name to be used for editing is returned. +func pushToEditRepositoryOrError(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) (string, error) { + // If editing the repository, no need to push anything + if editRepo == ctx.Repo.Repository { + return ctx.Repo.BranchName, nil + } + + // If editing a user fork, first push the branch to that repository + baseRepo := ctx.Repo.Repository + baseBranchName := ctx.Repo.BranchName + + log.Trace("pushBranchToUserRepo: pushing branch to user repo for editing: %s:%s %s:%s", baseRepo.FullName(), baseBranchName, editRepo.FullName(), branchName) + + if err := git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: editRepo.RepoPath(), + Branch: baseBranchName + ":" + branchName, + Env: repo_module.PushingEnvironment(ctx.Doer, editRepo), + }); err != nil { + ctx.RenderWithErr(ctx.Tr("repo.editor.fork_failed_to_push_branch", branchName), tpl, form) + return "", err + } + + return branchName, nil +} + +// updateEditRepositoryIsEmpty updates the the edit repository to mark it as no longer empty +func updateEditRepositoryIsEmpty(ctx *context.Context, editRepo *repo_model.Repository) { + if !editRepo.IsEmpty { + return + } + + editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) + if err != nil { + log.Error("gitrepo.OpenRepository: %v", err) + return + } + if editGitRepo == nil { + return + } + + if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") + } + editGitRepo.Close() +} + +func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + filePath, _ := path.Split(ctx.Req.URL.Query().Get("filename")) + filePath = strings.Trim(filePath, "/") + treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) + + ctx.Data["EditOperation"] = editOperation + ctx.Data["TreePath"] = treePath + ctx.Data["TreeNames"] = treeNames + ctx.Data["TreePaths"] = treePaths + ctx.Data["CanForkRepo"] = notEditableMessage == nil + ctx.Data["NotEditableMessage"] = notEditableMessage +} + +func ForkToEditFilePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) + + editRepo, notEditableMessage := getEditRepository(ctx) + + ctx.Data["PageHasPosted"] = true + + // Fork repository, if it doesn't already exist + if editRepo == nil && notEditableMessage == nil { + forkRepo := forkRepositoryOrError(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: getUniqueRepositoryName(ctx, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.BranchName, + }, tplForkFile, form) + if forkRepo == nil { + forkToEditFileCommon(ctx, form.EditOperation, form.TreePath, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) + return + } + } + + // Redirect back to editing page + ctx.Redirect(path.Join(ctx.Repo.RepoLink, form.EditOperation, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(form.TreePath))) +} + +// getUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func getUniqueRepositoryName(ctx *context.Context, name string) string { + uniqueName := name + i := 1 + + for { + _, err := repo_model.GetRepositoryByName(ctx, ctx.Doer.ID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } +} diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index dbb252a48cbf7..9bdf6471758af 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -24,7 +24,7 @@ const ( // NewDiffPatch render create patch page func NewDiffPatch(ctx *context.Context) { - editRepo := GetEditRepositoryOrFork(ctx, "_diffpatch") + editRepo := getEditRepositoryOrFork(ctx, "_diffpatch") if editRepo == nil { return } @@ -71,7 +71,7 @@ func NewDiffPatchPost(ctx *context.Context) { return } - editRepo := GetEditRepositoryOrError(ctx, tplPatchFile, &form) + editRepo := getEditRepositoryOrError(ctx, tplPatchFile, &form) if editRepo == nil { return } @@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) { renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !CheckCanPushEditBranch(ctx, editRepo, branchName, tplPatchFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, tplPatchFile, &form) { return } @@ -102,7 +102,7 @@ func NewDiffPatchPost(ctx *context.Context) { return } - editBranchName, err := PushEditBranchOrError(ctx, editRepo, branchName, tplPatchFile, &form) + editBranchName, err := pushToEditRepositoryOrError(ctx, editRepo, branchName, tplPatchFile, &form) if err != nil { return } From a1e173060c5ae9b6779b1ee3c10043b99ab97b20 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 23 Apr 2025 14:48:34 +0200 Subject: [PATCH 08/22] Remove change from earlier version of the implementation --- routers/web/repo/fork.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index fcb883d0f7a7d..946523f40954f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -125,11 +125,6 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { // Fork render repository fork page func Fork(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_fork") - - redirectTo := ctx.FormString("redirect_to_edit") - ctx.Data["ForkForEdit"] = len(redirectTo) > 0 - ctx.Data["ForkRedirectTo"] = redirectTo - getForkRepository(ctx) if ctx.Written() { return From d3f4f9db751cdec42062fc4a265594c98e12d121 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Thu, 24 Apr 2025 03:45:24 +0200 Subject: [PATCH 09/22] Don't allow committing to existing branch when choosing new branch --- routers/web/repo/editor.go | 6 +++--- routers/web/repo/fork_to_edit.go | 6 +++--- routers/web/repo/patch.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 0274bcf677c9a..d5b4b679cdf50 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -272,7 +272,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !canPushToEditRepository(ctx, editRepo, branchName, tplEditFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, form.CommitChoice, tplEditFile, &form) { return } @@ -512,7 +512,7 @@ func DeleteFilePost(ctx *context.Context) { renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !canPushToEditRepository(ctx, editRepo, branchName, tplDeleteFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, form.CommitChoice, tplDeleteFile, &form) { return } @@ -710,7 +710,7 @@ func UploadFilePost(ctx *context.Context) { renderCommitRights(ctx, editRepo) - if !canPushToEditRepository(ctx, editRepo, branchName, tplUploadFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, form.CommitChoice, tplUploadFile, &form) { return } diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index b17454736c172..c986482492c8d 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -109,9 +109,9 @@ func getEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form // CheckPushEditBranch chesk if pushing to the branch in the edit repository is possible, // and if not renders an error and returns false. -func canPushToEditRepository(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) bool { - // When pushing to a fork or another branch on the same repository, it should not exist yet - if ctx.Repo.Repository != editRepo || ctx.Repo.BranchName != branchName { +func canPushToEditRepository(ctx *context.Context, editRepo *repo_model.Repository, branchName, commitChoice string, tpl templates.TplName, form any) bool { + // When pushing to a fork or chosing to commit to a new branch, it should not exist yet + if ctx.Repo.Repository != editRepo || commitChoice == frmCommitChoiceNewBranch { if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index 9bdf6471758af..f673f0469af8d 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) { renderCommitRights(ctx, editRepo) // Cannot commit to an existing branch if user doesn't have rights - if !canPushToEditRepository(ctx, editRepo, branchName, tplPatchFile, &form) { + if !canPushToEditRepository(ctx, editRepo, branchName, form.CommitChoice, tplPatchFile, &form) { return } From bc2d5aae683d1d78b3370452c6e1853b3f07c219 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Thu, 24 Apr 2025 04:06:36 +0200 Subject: [PATCH 10/22] Share more checks with regular fork code, no reason not to --- routers/web/repo/fork.go | 59 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 946523f40954f..699e67bd0967f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -155,17 +155,30 @@ func ForkPost(ctx *context.Context) { return } + repo := forkRepositoryOrError(ctx, ctxUser, repo_service.ForkRepoOptions{ + BaseRepo: forkRepo, + Name: form.RepoName, + Description: form.Description, + SingleBranch: form.ForkSingleBranch, + }, tplFork, form) + if repo == nil { + return + } + + ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func forkRepositoryOrError(ctx *context.Context, user *user_model.User, opts repo_service.ForkRepoOptions, tpl templates.TplName, form any) *repo_model.Repository { var err error - traverseParentRepo := forkRepo + traverseParentRepo := opts.BaseRepo for { - if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) - return + if !repository.CanUserForkBetweenOwners(user.ID, traverseParentRepo.OwnerID) { + ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tpl, form) + return nil } - repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) + repo := repo_model.GetForkedRepo(ctx, user.ID, traverseParentRepo.ID) if repo != nil { - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) - return + return repo } if !traverseParentRepo.IsFork { break @@ -173,36 +186,22 @@ func ForkPost(ctx *context.Context) { traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) if err != nil { ctx.ServerError("GetRepositoryByID", err) - return + return nil } } // 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 user.IsOrganization() { + isAllowedToFork, err := organization.OrgFromUser(user).CanCreateOrgRepo(ctx, ctx.Doer.ID) if err != nil { ctx.ServerError("CanCreateOrgRepo", err) - return + return nil } else if !isAllowedToFork { ctx.HTTPError(http.StatusForbidden) - return + return nil } } - repo := forkRepositoryOrError(ctx, ctxUser, repo_service.ForkRepoOptions{ - BaseRepo: forkRepo, - Name: form.RepoName, - Description: form.Description, - SingleBranch: form.ForkSingleBranch, - }, tplFork, form) - if repo == nil { - return - } - - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) -} - -func forkRepositoryOrError(ctx *context.Context, user *user_model.User, opts repo_service.ForkRepoOptions, tpl templates.TplName, form any) *repo_model.Repository { repo, err := repo_service.ForkRepository(ctx, ctx.Doer, user, opts) if err != nil { ctx.Data["Err_RepoName"] = true @@ -210,9 +209,9 @@ func forkRepositoryOrError(ctx *context.Context, user *user_model.User, opts rep case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := user.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tpl, &form) + ctx.RenderWithErr(msg, tpl, form) case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tpl, &form) + ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tpl, form) case repo_model.IsErrRepoFilesAlreadyExist(err): switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): @@ -225,9 +224,9 @@ func forkRepositoryOrError(ctx *context.Context, user *user_model.User, opts rep ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, &form) + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, &form) + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) case errors.Is(err, user_model.ErrBlockedUser): ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tpl, form) default: From 333ff0eea0a55ec8816f355e501e664c42fc4f48 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Thu, 24 Apr 2025 03:49:23 +0200 Subject: [PATCH 11/22] Add a few more tests for failure cases --- tests/integration/editor_test.go | 110 ++++++++++++++++++++----- tests/integration/repo_webhook_test.go | 4 +- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index b47f4cb7c43ba..b9a648ea3f53a 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -31,7 +31,12 @@ import ( func TestCreateFile(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test.txt", "Content") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test.txt", "Content", "") + testCreateFile( + t, session, "user2", "user2", "repo1", "master", "master", "direct", "test.txt", "Content", + `A file named "test.txt" already exists in this repository.`) + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "commit-to-new-branch", "test2.txt", "Content", + `Branch "master" already exists in this repository.`) }) } @@ -39,11 +44,11 @@ func TestCreateFileFork(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user4") forkToEdit(t, session, "user2", "repo1", "_new", "master", "test.txt") - testCreateFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "test.txt", "Content") + testCreateFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "test.txt", "Content", "") }) } -func testCreateFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath, content string) { +func testCreateFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath, content, expectedError string) { // Request editor page newURL := fmt.Sprintf("/%s/%s/_new/%s/", owner, repo, branch) req := NewRequest(t, "GET", newURL) @@ -62,6 +67,16 @@ func testCreateFile(t *testing.T, session *TestSession, user, owner, repo, branc "commit_choice": commitChoice, "new_branch_name": targetBranch, }) + + if expectedError != "" { + resp := session.MakeRequest(t, req, http.StatusOK) + + // Check for expextecd error message + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, htmlDoc.doc.Find(".ui.flash-message").Text(), expectedError) + return + } + session.MakeRequest(t, req, http.StatusSeeOther) // Check new file exists @@ -222,7 +237,9 @@ func TestEditFileToNewBranchFork(t *testing.T) { func TestDeleteFile(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - testDeleteFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "README.md") + testDeleteFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "README.md", "") + testDeleteFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "MISSING.md", + `The file being deleted, "MISSING.md", no longer exists in this repository.`) }) } @@ -230,25 +247,29 @@ func TestDeleteFileFork(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user4") forkToEdit(t, session, "user2", "repo1", "_delete", "master", "README.md") - testDeleteFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "README.md") + testDeleteFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "commit-to-new-branch", "README.md", "") + testDeleteFile(t, session, "user4", "user2", "repo1", "master", "feature/missing", "commit-to-new-branch", "MISSING.md", + `The file being deleted, "MISSING.md", no longer exists in this repository.`) }) } -func testDeleteFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath string) { - // Check file exists - req := NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", owner, repo, branch, filePath) - session.MakeRequest(t, req, http.StatusOK) +func testDeleteFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, commitChoice, filePath, expectedError string) { + if expectedError == "" { + // Check file exists + req := NewRequestf(t, "GET", "/%s/%s/src/branch/%s/%s", owner, repo, branch, filePath) + session.MakeRequest(t, req, http.StatusOK) + } // Request editor page newURL := fmt.Sprintf("/%s/%s/_delete/%s/%s", owner, repo, branch, filePath) - req = NewRequest(t, "GET", newURL) + req := NewRequest(t, "GET", newURL) resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) lastCommit := doc.GetInputValueByName("last_commit") assert.NotEmpty(t, lastCommit) - // Save new file to master branch + // Save deleted file to target branch req = NewRequestWithValues(t, "POST", newURL, map[string]string{ "_csrf": doc.GetCSRF(), "last_commit": lastCommit, @@ -256,6 +277,16 @@ func testDeleteFile(t *testing.T, session *TestSession, user, owner, repo, branc "commit_choice": commitChoice, "new_branch_name": targetBranch, }) + + if expectedError != "" { + resp := session.MakeRequest(t, req, http.StatusOK) + + // Check for expextecd error message + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, htmlDoc.doc.Find(".ui.flash-message").Text(), expectedError) + return + } + session.MakeRequest(t, req, http.StatusSeeOther) // Check file was deleted @@ -266,7 +297,11 @@ func testDeleteFile(t *testing.T, session *TestSession, user, owner, repo, branc func TestPatchFile(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - testPatchFile(t, session, "user2", "user2", "repo1", "master", "feature/test") + testPatchFile(t, session, "user2", "user2", "repo1", "master", "feature/test", "Contents", "") + testPatchFile(t, session, "user2", "user2", "repo1", "master", "feature/test", "Contents", + `Branch "feature/test" already exists in this repository.`) + testPatchFile(t, session, "user2", "user2", "repo1", "feature/test", "feature/again", "Conflict", + `Unable to apply patch`) }) } @@ -274,11 +309,11 @@ func TestPatchFileFork(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user4") forkToEdit(t, session, "user2", "repo1", "_diffpatch", "master", "README.md") - testPatchFile(t, session, "user4", "user2", "repo1", "master", "feature/test") + testPatchFile(t, session, "user4", "user2", "repo1", "master", "feature/test", "Contents", "") }) } -func testPatchFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch string) { +func testPatchFile(t *testing.T, session *TestSession, user, owner, repo, branch, targetBranch, contents, expectedError string) { // Request editor page newURL := fmt.Sprintf("/%s/%s/_diffpatch/%s/", owner, repo, branch) req := NewRequest(t, "GET", newURL) @@ -293,17 +328,27 @@ func testPatchFile(t *testing.T, session *TestSession, user, owner, repo, branch "_csrf": doc.GetCSRF(), "last_commit": lastCommit, "tree_path": "__dummy__", - "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt + "content": fmt.Sprintf(`diff --git a/patch-file-1.txt b/patch-file-1.txt new file mode 100644 index 0000000000..aaaaaaaaaa --- /dev/null +++ b/patch-file-1.txt @@ -0,0 +1 @@ -+File 1 -`, ++%s +`, contents), "commit_choice": "commit-to-new-branch", "new_branch_name": targetBranch, }) + + if expectedError != "" { + resp := session.MakeRequest(t, req, http.StatusOK) + + // Check for expextecd error message + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, htmlDoc.doc.Find(".ui.flash-message").Text(), expectedError) + return + } + session.MakeRequest(t, req, http.StatusSeeOther) // Check new file exists @@ -482,7 +527,7 @@ func forkToEdit(t *testing.T, session *TestSession, owner, repo, operation, bran assert.Equal(t, "/"+path.Join(owner, repo, operation, branch, filePath), test.RedirectURL(resp)) } -func testForkToEdit(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { +func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { // Fork repository because we can't edit it forkToEdit(t, session, owner, repo, "_edit", branch, filePath) @@ -524,9 +569,34 @@ func testForkToEdit(t *testing.T, session *TestSession, user, owner, repo, branc session.MakeRequest(t, req, http.StatusOK) } -func TestForkToEdit(t *testing.T) { +func TestForkToEditFile(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user4") + testForkToEditFile(t, session, "user4", "user2", "repo1", "master", "README.md") + }) +} + +func TestEditFileNotAllowed(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user4") - testForkToEdit(t, session, "user4", "user2", "repo1", "master", "README.md") + + operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch", "_cherrypick"} + + for _, operation := range operations { + // Branch does not exist + url := path.Join("user2", "repo1", operation, "missing", "README.md") + req := NewRequest(t, "GET", url) + session.MakeRequest(t, req, http.StatusNotFound) + + // Private repository + url = path.Join("user2", "repo2", operation, "master", "Home.md") + req = NewRequest(t, "GET", url) + session.MakeRequest(t, req, http.StatusNotFound) + + // Empty repository + url = path.Join("org41", "repo61", operation, "master", "README.md") + req = NewRequest(t, "GET", url) + session.MakeRequest(t, req, http.StatusNotFound) + } }) } diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index bb4747ccd0e26..8395c0622c73d 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -310,7 +310,7 @@ func Test_WebhookPush(t *testing.T) { testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push") // 2. trigger the webhook - testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push", "") // 3. validate the webhook is triggered assert.Equal(t, "push", triggeredEvent) @@ -602,7 +602,7 @@ func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only") // 2. trigger the webhook with a push action - testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push") + testCreateFile(t, session, "user2", "user2", "repo1", "master", "master", "direct", "test_webhook_push.md", "# a test file for webhook push", "") // 3. validate the webhook is triggered with right event assert.Equal(t, "push", trigger) From b51ff012ca65f62e00d0fe150f5eb26795225424 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Thu, 24 Apr 2025 04:44:42 +0200 Subject: [PATCH 12/22] Fix broken test due to stricter new branch check --- tests/integration/pull_status_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 715d41bb3cb1b..d4d631da57a18 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -124,7 +124,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "master", "status1", "README.md", "status1") - testEditFileToNewBranch(t, session, "user1", "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") + testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, From 2410c4325cba38a7b4d5aa76c7e0e0011dbbb0ca Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Thu, 24 Apr 2025 23:51:27 +0200 Subject: [PATCH 13/22] Compare repositories by ID, add Gitea copyright to new file --- routers/web/repo/editor.go | 3 ++- routers/web/repo/fork_to_edit.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index d5b4b679cdf50..ffbe377c47e1e 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo @@ -46,7 +47,7 @@ func canCreateBasePullRequest(ctx *context.Context, editRepo *repo_model.Reposit } func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) bool { - if editRepo == ctx.Repo.Repository { + if editRepo.ID == ctx.Repo.Repository.ID { // Editing the same repository that we are viewing canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index c986482492c8d..ba949391af91d 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo @@ -111,7 +112,7 @@ func getEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form // and if not renders an error and returns false. func canPushToEditRepository(ctx *context.Context, editRepo *repo_model.Repository, branchName, commitChoice string, tpl templates.TplName, form any) bool { // When pushing to a fork or chosing to commit to a new branch, it should not exist yet - if ctx.Repo.Repository != editRepo || commitChoice == frmCommitChoiceNewBranch { + if editRepo.ID != ctx.Repo.Repository.ID || commitChoice == frmCommitChoiceNewBranch { if exist, err := git_model.IsBranchExist(ctx, editRepo.ID, branchName); err == nil && exist { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tpl, form) @@ -137,8 +138,8 @@ func canPushToEditRepository(ctx *context.Context, editRepo *repo_model.Reposito // to the user fork, if needed. On failure, it displays and returns an error. The // branch name to be used for editing is returned. func pushToEditRepositoryOrError(ctx *context.Context, editRepo *repo_model.Repository, branchName string, tpl templates.TplName, form any) (string, error) { - // If editing the repository, no need to push anything - if editRepo == ctx.Repo.Repository { + // If editing the same repository, no need to push anything + if editRepo.ID == ctx.Repo.Repository.ID { return ctx.Repo.BranchName, nil } From 6ba37a04cf6cc861707339a2f0c1717ca82f2437 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Fri, 25 Apr 2025 00:05:30 +0200 Subject: [PATCH 14/22] Fix missing diff preview, revert order to what it was before, add test --- routers/web/web.go | 9 +++++---- tests/integration/editor_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/routers/web/web.go b/routers/web/web.go index aa921e0e81dfc..73b1a7696e651 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1314,16 +1314,17 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo code m.Group("", func() { m.Group("", func() { - m.Combo("/_new/*").Get(repo.NewFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) + m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) m.Combo("/_edit/*").Get(repo.EditFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) + m.Combo("/_new/*").Get(repo.NewFile). + Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) m.Combo("/_delete/*").Get(repo.DeleteFile). Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/_upload/*", context.MustBeAbleToUpload()).Get(repo.UploadFile). Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) + m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). + Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/_fork_to_edit/*"). Post(web.Bind(forms.ForkToEditRepoFileForm{}), repo.ForkToEditFilePost) }, context.MustEnableEditor()) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index b9a648ea3f53a..3c54228a134de 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -234,6 +234,34 @@ func TestEditFileToNewBranchFork(t *testing.T) { }) } +func testEditFileDiffPreview(t *testing.T, session *TestSession, user, repo, branch, filePath string) { + // Get to the 'edit this file' page + req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + lastCommit := htmlDoc.GetInputValueByName("last_commit") + assert.NotEmpty(t, lastCommit) + + // Preview the changes + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_preview", branch, filePath), + map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "content": "Hello, World (Edited)\n", + }, + ) + resp = session.MakeRequest(t, req, http.StatusOK) + + assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`) +} + +func TestEditFileDiffPreview(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") + testEditFileDiffPreview(t, session, "user2", "repo1", "master", "README.md") + }) +} + func TestDeleteFile(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") From f2fc2051a6054d5296cef4814170e52e15ac3d66 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Sun, 27 Apr 2025 17:06:28 +0200 Subject: [PATCH 15/22] Fix translation lookup error due to missing WontSignReason --- routers/web/repo/editor.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index ffbe377c47e1e..ec6b9c37d2bc8 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -47,24 +47,24 @@ func canCreateBasePullRequest(ctx *context.Context, editRepo *repo_model.Reposit } func renderCommitRights(ctx *context.Context, editRepo *repo_model.Repository) bool { + canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, editRepo, ctx.Repo.BranchName) + if err != nil { + log.Error("CanCommitToBranch: %v", err) + } + if editRepo.ID == ctx.Repo.Repository.ID { // Editing the same repository that we are viewing - canCommitToBranch, err := context.CanCommitToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) - if err != nil { - log.Error("CanCommitToBranch: %v", err) - } - - ctx.Data["CanCommitToBranch"] = canCommitToBranch ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx, editRepo) - - return canCommitToBranch.CanCommitToBranch + } else { + // Editing a user fork of the repository we are viewing, always choose a new branch + canCommitToBranch.CanCommitToBranch = false + canCommitToBranch.UserCanPush = false + ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) } - // Editing a user fork of the repository we are viewing, always choose a new branch - ctx.Data["CanCommitToBranch"] = context.CanCommitToBranchResults{} - ctx.Data["CanCreatePullRequest"] = canCreateBasePullRequest(ctx, editRepo) + ctx.Data["CanCommitToBranch"] = canCommitToBranch - return false + return canCommitToBranch.CanCommitToBranch } // redirectForCommitChoice redirects after committing the edit to a branch From 170d6b6548fc81554b602571dfc31ef8acc4f7a7 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Tue, 29 Apr 2025 21:41:31 +0200 Subject: [PATCH 16/22] Disable new/edit/delete button when not signed in Also related tweaks for consistency: * Hide new file button when repository is archived or mirrored, matching edit and delete buttons * Show readme edit button also when not editable, and disable it, to match the other edit button. --- options/locale/locale_en-US.ini | 1 + routers/web/repo/view_file.go | 10 ++++++++-- routers/web/repo/view_readme.go | 8 +++++++- templates/repo/view_content.tmpl | 5 +++-- templates/repo/view_file.tmpl | 8 ++++++-- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7f57ccbf44378..8848afc2f37e5 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1336,6 +1336,7 @@ editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. editor.delete_this_file = Delete File editor.must_have_write_access = You must have write access to make or propose changes to this file. +editor.must_be_signed_in = You must be signed in to make or propose changes. editor.file_delete_success = File "%s" has been deleted. editor.name_your_file = Name your file⦠editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 5a6dee4af732f..dd6369901fbdf 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -245,7 +245,10 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } if !fInfo.isLFSFile { if ctx.Repo.CanEnableEditor() { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + if !ctx.IsSigned { + ctx.Data["CanEditFile"] = false + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { ctx.Data["CanEditFile"] = false ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") } else { @@ -306,7 +309,10 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } if ctx.Repo.CanEnableEditor() { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + if !ctx.IsSigned { + ctx.Data["CanDeleteFile"] = false + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { ctx.Data["CanDeleteFile"] = false ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") } else { diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 32810b3b8bf5b..d92e7ec5760a3 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -214,7 +214,13 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil if !fInfo.isLFSFile { if ctx.Repo.CanEnableEditor() { - ctx.Data["CanEditReadmeFile"] = true + if !ctx.IsSigned { + ctx.Data["CanEditReadmeFile"] = false + ctx.Data["EditReadmeFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") + } else { + ctx.Data["CanEditReadmeFile"] = true + ctx.Data["EditReadmeFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") + } } } } diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 65d1c5d2846e7..1b6a02a1dc241 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,8 +41,9 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .RefFullName.IsBranch (not .IsViewFile)}} - <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> + {{if and .RefFullName.IsBranch (not .IsViewFile) .Repository.CanEnableEditor}} + <button class="ui dropdown basic compact jump button" + {{if not .IsSigned}} disabled data-tooltip-content="{{ctx.Locale.Tr "repo.editor.must_be_signed_in"}}"{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index f01adccadc5de..2d891e5b164a8 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -77,8 +77,12 @@ <button class="ui mini basic button unescape-button tw-mr-1 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> <button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> {{end}} - {{if and .ReadmeInList .CanEditReadmeFile}} - <a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .FileTreePath}}">{{svg "octicon-pencil"}}</a> + {{if and .ReadmeInList .Repository.CanEnableEditor}} + {{if .CanEditReadmeFile}} + <a class="btn-octicon" data-tooltip-content="{{.EditReadmeFileTooltip}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .FileTreePath}}">{{svg "octicon-pencil"}}</a> + {{else}} + <span class="btn-octicon disabled" data-tooltip-content="{{.EditReadmeFileTooltip}}">{{svg "octicon-pencil"}}</span> + {{end}} {{end}} </div> </h4> From 14549b836bccb9f51210ed10f6fe7b2995cbf1cb Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 7 May 2025 00:21:31 +0200 Subject: [PATCH 17/22] Address comments from wxiaoguang --- routers/web/repo/fork_to_edit.go | 16 +++------------- templates/repo/editor/fork.tmpl | 1 - templates/repo/view_content.tmpl | 5 ++--- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index ba949391af91d..62febda70816d 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "path" - "strings" git_model "code.gitea.io/gitea/models/git" access_model "code.gitea.io/gitea/models/perm/access" @@ -167,27 +166,20 @@ func updateEditRepositoryIsEmpty(ctx *context.Context, editRepo *repo_model.Repo return } - editGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, editRepo) + editGitRepo, err := gitrepo.OpenRepository(ctx, editRepo) if err != nil { log.Error("gitrepo.OpenRepository: %v", err) return } - if editGitRepo == nil { - return - } + defer editGitRepo.Close() if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") } - editGitRepo.Close() } func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, _ := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) + treeNames, treePaths := getParentTreeFields(treePath) ctx.Data["EditOperation"] = editOperation ctx.Data["TreePath"] = treePath @@ -202,8 +194,6 @@ func ForkToEditFilePost(ctx *context.Context) { editRepo, notEditableMessage := getEditRepository(ctx) - ctx.Data["PageHasPosted"] = true - // Fork repository, if it doesn't already exist if editRepo == nil && notEditableMessage == nil { forkRepo := forkRepositoryOrError(ctx, ctx.Doer, repo_service.ForkRepoOptions{ diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl index f89bf2f3f2f72..46e927825f13a 100644 --- a/templates/repo/editor/fork.tmpl +++ b/templates/repo/editor/fork.tmpl @@ -5,7 +5,6 @@ {{template "base/alert" .}} <form class="ui edit form" method="post" action="{{.RepoLink}}/_fork_to_edit/{{.BranchName | PathEscapeSegments}}"> {{.CsrfTokenHtml}} - <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> <div class="repo-editor-header"> <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}"> <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 1b6a02a1dc241..303b1ab9b3e56 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,9 +41,8 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .RefFullName.IsBranch (not .IsViewFile) .Repository.CanEnableEditor}} - <button class="ui dropdown basic compact jump button" - {{if not .IsSigned}} disabled data-tooltip-content="{{ctx.Locale.Tr "repo.editor.must_be_signed_in"}}"{{end}}> + {{if and .RefFullName.IsBranch (not .IsViewFile) .Repository.CanEnableEditor .IsSigned}} + <button class="ui dropdown basic compact jump button"> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> From 693e600c83da3c72ab5e929688f239fc89b69e3a Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 7 May 2025 01:25:30 +0200 Subject: [PATCH 18/22] Remove unused ids in fork template --- templates/repo/editor/fork.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl index 46e927825f13a..a056fa3d3bb94 100644 --- a/templates/repo/editor/fork.tmpl +++ b/templates/repo/editor/fork.tmpl @@ -18,8 +18,8 @@ <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> {{end}} {{end}} - <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> - <input type="hidden" id="edit_operation" name="edit_operation" value="{{.EditOperation}}" required> + <input type="hidden" name="tree_path" value="{{.TreePath}}" required> + <input type="hidden" name="edit_operation" value="{{.EditOperation}}" required> </div> </div> <div class="field center"> From e78c6c463f3491b8401fcdf49f82a02fcd2eb041 Mon Sep 17 00:00:00 2001 From: wxiaoguang <wxiaoguang@gmail.com> Date: Wed, 7 May 2025 08:22:24 +0800 Subject: [PATCH 19/22] markRepositoryAsNonEmpty --- routers/web/repo/editor.go | 4 ++-- routers/web/repo/fork_to_edit.go | 17 +++-------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 10bb6f75b3b6b..5c834dfb3e2d8 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -402,7 +402,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } } - updateEditRepositoryIsEmpty(ctx, editRepo) + markRepositoryAsNonEmpty(ctx, editRepo) // a file has been edited, so the repository is no longer empty redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } @@ -829,7 +829,7 @@ func UploadFilePost(ctx *context.Context) { return } - updateEditRepositoryIsEmpty(ctx, editRepo) + markRepositoryAsNonEmpty(ctx, editRepo) // a file has been uploaded, so the repository is no longer empty redirectForCommitChoice(ctx, editRepo, form.CommitChoice, branchName, form.TreePath) } diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index 62febda70816d..ab461c2b231a1 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -14,7 +14,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/templates" @@ -160,22 +159,12 @@ func pushToEditRepositoryOrError(ctx *context.Context, editRepo *repo_model.Repo return branchName, nil } -// updateEditRepositoryIsEmpty updates the the edit repository to mark it as no longer empty -func updateEditRepositoryIsEmpty(ctx *context.Context, editRepo *repo_model.Repository) { +// markRepositoryAsNonEmpty marks the repository as no longer empty +func markRepositoryAsNonEmpty(ctx *context.Context, editRepo *repo_model.Repository) { if !editRepo.IsEmpty { return } - - editGitRepo, err := gitrepo.OpenRepository(ctx, editRepo) - if err != nil { - log.Error("gitrepo.OpenRepository: %v", err) - return - } - defer editGitRepo.Close() - - if isEmpty, err := editGitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") - } + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") } func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { From e92d6a2e47d1eba0d5d29c7c44b51f6a805cfcc5 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 7 May 2025 20:56:23 +0200 Subject: [PATCH 20/22] Use ctx.ServerError instead of custom message --- options/locale/locale_en-US.ini | 1 - routers/web/repo/fork_to_edit.go | 71 ++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 48deafc87add4..2162fe4649c3a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1401,7 +1401,6 @@ editor.fork_edit_description = You can not edit this repository directly. The ch editor.fork_failed_to_push_branch = Failed to push branch %s to your repoitory. editor.cannot_find_editable_repo = Can not find repository to apply the edit to. Was it deleted while editing? editor.fork_not_editable = Fork Repository Not Editable -editor.fork_internal_error = Internal error loading repository information about <b>%s</b>. editor.fork_is_archived = Your repository <b>%s</b> is archived. Unarchive it in repository settings to make changes. editor.fork_code_disabled = Code is disabled in your repository <b>%s</b>. Enable code in repository settings to make changes. editor.fork_no_permission = You do not have permission to write to repository <b>%s</b>. diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index ab461c2b231a1..9d6690beffd4d 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -31,78 +31,85 @@ const ( // getEditRepository returns the repository where the actual edits will be written to. // This may be a fork of the repository owned by the user. If no repository can be found // for editing, nil is returned along with a message explaining why editing is not possible. -func getEditRepository(ctx *context.Context) (*repo_model.Repository, any) { +func getEditRepository(ctx *context.Context) (*repo_model.Repository, any, error) { if context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) { - return ctx.Repo.Repository, nil + return ctx.Repo.Repository, nil, nil } // If we can't write to the branch, try find a user fork to create a branch in instead userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) if err != nil { - log.Error("GetUserFork: %v", err) - return nil, nil + return nil, nil, fmt.Errorf("GetUserFork: %v", err) } if userRepo == nil { - return nil, nil + return nil, nil, nil } // Load repository information if err := userRepo.LoadOwner(ctx); err != nil { - log.Error("LoadOwner: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + return nil, nil, fmt.Errorf("LoadOwner: v", err) } if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { if err != nil { - log.Error("GetBaseRepo: %v", err) - } else { - log.Error("GetBaseRepo: Expected a base repo for user fork", err) + return nil, nil, fmt.Errorf("GetBaseRepo: %v", err) } - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + return nil, nil, fmt.Errorf("GetBaseRepo: Expected a base repo for user fork") } // Check code unit, archiving and permissions. if !userRepo.UnitEnabled(ctx, unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()) + return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()), nil } if userRepo.IsArchived { - return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()) + return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()), nil } permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) if err != nil { - log.Error("access_model.GetUserRepoPermission: %v", err) - return nil, ctx.Tr("repo.editor.fork_internal_error", userRepo.FullName()) + return nil, nil, fmt.Errorf("access_model.GetUserRepoPermission: %v", err) } if !permission.CanWrite(unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()) + return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()), nil } - ctx.Data["ForkRepo"] = userRepo - return userRepo, nil + return userRepo, nil, nil } -// GetEditRepository returns the repository where the edits will be written to. +// getEditRepositoryOrFork returns the repository where the edits will be written to. // If no repository is editable, redirects to a page to create a fork. func getEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_model.Repository { - editRepo, notEditableMessage := getEditRepository(ctx) - if editRepo != nil { - return editRepo + editRepo, notEditableMessage, err := getEditRepository(ctx) + if err != nil { + ctx.ServerError("getEditRepository", err) + return nil } - - // No editable repository, suggest to create a fork - forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) - ctx.HTML(http.StatusOK, tplForkFile) - return nil + if editRepo == nil { + // No editable repository, suggest to create a fork + forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) + ctx.HTML(http.StatusOK, tplForkFile) + return nil + } + if editRepo.ID != ctx.Repo.Repository.ID { + ctx.Data["ForkRepo"] = editRepo + } + return editRepo } -// GetEditRepository returns the repository where the edits will be written to. +// getEditRepositoryOrError returns the repository where the edits will be written to. // If no repository is editable, display an error. func getEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form any) *repo_model.Repository { - editRepo, _ := getEditRepository(ctx) + editRepo, _, err := getEditRepository(ctx) + if err != nil { + ctx.ServerError("getEditRepository", err) + return nil + } if editRepo == nil { // No editable repo, maybe the fork was deleted in the meantime ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_find_editable_repo"), tpl, form) return nil } + if editRepo.ID != ctx.Repo.Repository.ID { + ctx.Data["ForkRepo"] = editRepo + } return editRepo } @@ -181,7 +188,11 @@ func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, func ForkToEditFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) - editRepo, notEditableMessage := getEditRepository(ctx) + editRepo, notEditableMessage, err := getEditRepository(ctx) + if err != nil { + ctx.ServerError("getEditRepository", err) + return + } // Fork repository, if it doesn't already exist if editRepo == nil && notEditableMessage == nil { From 6ff820aed6b536a34efea5d356871c1fe25d5def Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Wed, 7 May 2025 21:29:56 +0200 Subject: [PATCH 21/22] Fix permission check for uploading files, move GetEditableRepository to context --- routers/web/repo/editor.go | 2 - routers/web/repo/fork_to_edit.go | 70 ++++++-------------------------- routers/web/web.go | 2 +- services/context/repo.go | 63 ++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 60 deletions(-) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 5c834dfb3e2d8..d40cd170331ab 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -847,7 +847,6 @@ func cleanUploadFileName(name string) string { } // UploadFileToServer upload file to server file dir not git -// This is independent of any repository, no repository permissions are checked to call this. func UploadFileToServer(ctx *context.Context) { file, header, err := ctx.Req.FormFile("file") if err != nil { @@ -887,7 +886,6 @@ func UploadFileToServer(ctx *context.Context) { } // RemoveUploadFileFromServer remove file from server file dir -// This is independent of any repository, no repository permissions are checked to call this. func RemoveUploadFileFromServer(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) if len(form.File) == 0 { diff --git a/routers/web/repo/fork_to_edit.go b/routers/web/repo/fork_to_edit.go index 9d6690beffd4d..6c6b8fa0f838d 100644 --- a/routers/web/repo/fork_to_edit.go +++ b/routers/web/repo/fork_to_edit.go @@ -10,9 +10,7 @@ import ( "path" git_model "code.gitea.io/gitea/models/git" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" @@ -28,63 +26,17 @@ const ( tplForkFile templates.TplName = "repo/editor/fork" ) -// getEditRepository returns the repository where the actual edits will be written to. -// This may be a fork of the repository owned by the user. If no repository can be found -// for editing, nil is returned along with a message explaining why editing is not possible. -func getEditRepository(ctx *context.Context) (*repo_model.Repository, any, error) { - if context.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) { - return ctx.Repo.Repository, nil, nil - } - - // If we can't write to the branch, try find a user fork to create a branch in instead - userRepo, err := repo_model.GetUserFork(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) - if err != nil { - return nil, nil, fmt.Errorf("GetUserFork: %v", err) - } - if userRepo == nil { - return nil, nil, nil - } - - // Load repository information - if err := userRepo.LoadOwner(ctx); err != nil { - return nil, nil, fmt.Errorf("LoadOwner: v", err) - } - if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { - if err != nil { - return nil, nil, fmt.Errorf("GetBaseRepo: %v", err) - } - return nil, nil, fmt.Errorf("GetBaseRepo: Expected a base repo for user fork") - } - - // Check code unit, archiving and permissions. - if !userRepo.UnitEnabled(ctx, unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_code_disabled", userRepo.FullName()), nil - } - if userRepo.IsArchived { - return nil, ctx.Tr("repo.editor.fork_is_archived", userRepo.FullName()), nil - } - permission, err := access_model.GetUserRepoPermission(ctx, userRepo, ctx.Doer) - if err != nil { - return nil, nil, fmt.Errorf("access_model.GetUserRepoPermission: %v", err) - } - if !permission.CanWrite(unit.TypeCode) { - return nil, ctx.Tr("repo.editor.fork_no_permission", userRepo.FullName()), nil - } - - return userRepo, nil, nil -} - // getEditRepositoryOrFork returns the repository where the edits will be written to. // If no repository is editable, redirects to a page to create a fork. func getEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_model.Repository { - editRepo, notEditableMessage, err := getEditRepository(ctx) + editRepo, msg, err := context.GetEditableRepository(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { ctx.ServerError("getEditRepository", err) return nil } if editRepo == nil { // No editable repository, suggest to create a fork - forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, notEditableMessage) + forkToEditFileCommon(ctx, editOperation, ctx.Repo.TreePath, msg) ctx.HTML(http.StatusOK, tplForkFile) return nil } @@ -97,7 +49,7 @@ func getEditRepositoryOrFork(ctx *context.Context, editOperation string) *repo_m // getEditRepositoryOrError returns the repository where the edits will be written to. // If no repository is editable, display an error. func getEditRepositoryOrError(ctx *context.Context, tpl templates.TplName, form any) *repo_model.Repository { - editRepo, _, err := getEditRepository(ctx) + editRepo, _, err := context.GetEditableRepository(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { ctx.ServerError("getEditRepository", err) return nil @@ -174,28 +126,32 @@ func markRepositoryAsNonEmpty(ctx *context.Context, editRepo *repo_model.Reposit _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: editRepo.ID, IsEmpty: false}, "is_empty") } -func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, notEditableMessage any) { +func forkToEditFileCommon(ctx *context.Context, editOperation, treePath string, msg context.RepositoryNotEditableMessage) { treeNames, treePaths := getParentTreeFields(treePath) ctx.Data["EditOperation"] = editOperation ctx.Data["TreePath"] = treePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["CanForkRepo"] = notEditableMessage == nil - ctx.Data["NotEditableMessage"] = notEditableMessage + + canForkRepo := msg.Reason == "" + ctx.Data["CanForkRepo"] = canForkRepo + if !canForkRepo { + ctx.Data["NotEditableMessage"] = ctx.Tr(msg.Reason, msg.Repository) + } } func ForkToEditFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ForkToEditRepoFileForm) - editRepo, notEditableMessage, err := getEditRepository(ctx) + editRepo, msg, err := context.GetEditableRepository(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if err != nil { ctx.ServerError("getEditRepository", err) return } // Fork repository, if it doesn't already exist - if editRepo == nil && notEditableMessage == nil { + if editRepo == nil && msg.Reason == "" { forkRepo := forkRepositoryOrError(ctx, ctx.Doer, repo_service.ForkRepoOptions{ BaseRepo: ctx.Repo.Repository, Name: getUniqueRepositoryName(ctx, ctx.Repo.Repository.Name), @@ -203,7 +159,7 @@ func ForkToEditFilePost(ctx *context.Context) { SingleBranch: ctx.Repo.BranchName, }, tplForkFile, form) if forkRepo == nil { - forkToEditFileCommon(ctx, form.EditOperation, form.TreePath, notEditableMessage) + forkToEditFileCommon(ctx, form.EditOperation, form.TreePath, msg) ctx.HTML(http.StatusOK, tplForkFile) return } diff --git a/routers/web/web.go b/routers/web/web.go index bdca68993de9c..4afc995e46807 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1336,7 +1336,7 @@ func registerWebRoutes(m *web.Router) { m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) - }, context.MustBeAbleToUpload()) + }, context.MustBeAbleToUpload(), context.MustHaveEditableRepository()) m.Group("/branches", func() { m.Group("/_new", func() { diff --git a/services/context/repo.go b/services/context/repo.go index 3ef7da5e68d99..4014d44db4c18 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -76,6 +76,58 @@ func CanWriteToBranch(ctx context.Context, user *user_model.User, repo *repo_mod return issues_model.CanMaintainerWriteToBranch(ctx, permission, branch, user) } +// RepositoryNotEditableMessage explains why a repository can not be edited by the user +type RepositoryNotEditableMessage struct { + Reason string + Repository string +} + +// GetEditRepository returns the repository where the editor will actually write the +// the edits to. This may be a fork of the repository owned by the user. If no repository +// can be found for editing, either a reason or error is returned. +func GetEditableRepository(ctx context.Context, user *user_model.User, repo *repo_model.Repository, branch string) (*repo_model.Repository, RepositoryNotEditableMessage, error) { + if CanWriteToBranch(ctx, user, repo, branch) { + return repo, RepositoryNotEditableMessage{}, nil + } + + // If we can't write to the branch, try find a user fork to create a branch in instead + userRepo, err := repo_model.GetUserFork(ctx, repo.ID, user.ID) + if err != nil { + return nil, RepositoryNotEditableMessage{}, fmt.Errorf("GetUserFork: %v", err) + } + if userRepo == nil { + return nil, RepositoryNotEditableMessage{}, nil + } + + // Load repository information + if err := userRepo.LoadOwner(ctx); err != nil { + return nil, RepositoryNotEditableMessage{}, fmt.Errorf("LoadOwner: %v", err) + } + if err := userRepo.GetBaseRepo(ctx); err != nil || userRepo.BaseRepo == nil { + if err != nil { + return nil, RepositoryNotEditableMessage{}, fmt.Errorf("GetBaseRepo: %v", err) + } + return nil, RepositoryNotEditableMessage{}, errors.New("GetBaseRepo: Expected a base repo for user fork") + } + + // Check code unit, archiving and permissions. + if !userRepo.UnitEnabled(ctx, unit_model.TypeCode) { + return nil, RepositoryNotEditableMessage{Reason: "repo.editor.fork_code_disabled", Repository: userRepo.FullName()}, nil + } + if userRepo.IsArchived { + return nil, RepositoryNotEditableMessage{Reason: "repo.editor.fork_is_archived", Repository: userRepo.FullName()}, nil + } + permission, err := access_model.GetUserRepoPermission(ctx, userRepo, user) + if err != nil { + return nil, RepositoryNotEditableMessage{}, fmt.Errorf("access_model.GetUserRepoPermission: %v", err) + } + if !permission.CanWrite(unit_model.TypeCode) { + return nil, RepositoryNotEditableMessage{Reason: "repo.editor.fork_no_permission", Repository: userRepo.FullName()}, nil + } + + return userRepo, RepositoryNotEditableMessage{}, nil +} + // CanEnableEditor returns true if the web editor can be enabled for this repository, // either by directly writing to the repository or to a user fork. func (r *Repository) CanEnableEditor() bool { @@ -118,6 +170,17 @@ func MustBeAbleToUpload() func(ctx *Context) { } } +// MustHaveEditableRepository checks that there exists a repository that can be written +// to by the user for editing. +func MustHaveEditableRepository() func(ctx *Context) { + return func(ctx *Context) { + editRepo, _, _ := GetEditableRepository(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) + if editRepo == nil { + ctx.NotFound(nil) + } + } +} + // CanCommitToBranchResults represents the results of CanCommitToBranch type CanCommitToBranchResults struct { CanCommitToBranch bool From eb10c4f79e022d941b3405f4609f1f641652d00f Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel <brecht@blender.org> Date: Tue, 13 May 2025 13:41:26 +0200 Subject: [PATCH 22/22] Further permission checks, centralized in CanEnableEditor --- routers/web/repo/pull.go | 1 + routers/web/repo/view_file.go | 7 +++++-- routers/web/repo/view_home.go | 1 + routers/web/repo/view_readme.go | 5 ++++- routers/web/web.go | 2 +- services/context/permission.go | 2 +- services/context/repo.go | 14 ++++---------- templates/repo/diff/box.tmpl | 2 +- templates/repo/empty.tmpl | 2 +- templates/repo/view_content.tmpl | 2 +- templates/repo/view_file.tmpl | 4 ++-- 11 files changed, 22 insertions(+), 20 deletions(-) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 43ddc265cfafb..4140a8da2413a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -915,6 +915,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) { + ctx.Data["CanEnableEditor"] = ctx.Repo.Repository.CanEnableEditor() ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link() diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index dd6369901fbdf..2dbf81e9b652b 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -147,6 +147,9 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") } + canEnableEditor := ctx.Repo.CanEnableEditor(ctx, ctx.Doer) + ctx.Data["CanEnableEditor"] = canEnableEditor + // read all needed attributes which will be used later // there should be no performance different between reading 2 or 4 here attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{ @@ -244,7 +247,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["LineEscapeStatus"] = statuses } if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor() { + if canEnableEditor { if !ctx.IsSigned { ctx.Data["CanEditFile"] = false ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") @@ -308,7 +311,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - if ctx.Repo.CanEnableEditor() { + if canEnableEditor { if !ctx.IsSigned { ctx.Data["CanDeleteFile"] = false ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 3b053821ee751..7ef80a08a4b16 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -405,6 +405,7 @@ func Home(ctx *context.Context) { ctx.Data["Title"] = title ctx.Data["PageIsViewCode"] = true ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + ctx.Data["CanEnableEditor"] = ctx.Repo.CanEnableEditor(ctx, ctx.Doer) if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { // empty or broken repositories need to be handled differently diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index d92e7ec5760a3..c5984d2a30b59 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -212,8 +212,11 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } + canEnableEditor := ctx.Repo.CanEnableEditor(ctx, ctx.Doer) + ctx.Data["CanEnableEditor"] = canEnableEditor + if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor() { + if canEnableEditor { if !ctx.IsSigned { ctx.Data["CanEditReadmeFile"] = false ctx.Data["EditReadmeFileTooltip"] = ctx.Tr("repo.editor.must_be_signed_in") diff --git a/routers/web/web.go b/routers/web/web.go index 4afc995e46807..bdca68993de9c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1336,7 +1336,7 @@ func registerWebRoutes(m *web.Router) { m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) - }, context.MustBeAbleToUpload(), context.MustHaveEditableRepository()) + }, context.MustBeAbleToUpload()) m.Group("/branches", func() { m.Group("/_new", func() { diff --git a/services/context/permission.go b/services/context/permission.go index 883db5e649258..bf6e498741799 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -24,7 +24,7 @@ func RequireRepoAdmin() func(ctx *Context) { // MustBeAbleToCherryPick checks if the user is allowed to cherry-pick to a branch of the repo func MustBeAbleToCherryPick() func(ctx *Context) { return func(ctx *Context) { - if !CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) || !ctx.Repo.Repository.CanEnableEditor() { + if !CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) || !ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { ctx.NotFound(nil) return } diff --git a/services/context/repo.go b/services/context/repo.go index 4014d44db4c18..a7213452d9e1d 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -130,8 +130,9 @@ func GetEditableRepository(ctx context.Context, user *user_model.User, repo *rep // CanEnableEditor returns true if the web editor can be enabled for this repository, // either by directly writing to the repository or to a user fork. -func (r *Repository) CanEnableEditor() bool { - return r.RefFullName.IsBranch() && r.Repository.CanEnableEditor() +func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { + return r.RefFullName.IsBranch() && r.Repository.CanEnableEditor() && + (CanWriteToBranch(ctx, user, r.Repository, r.BranchName) || (r.Permission.CanRead(unit_model.TypeCode) && r.Permission.CanRead(unit_model.TypePullRequests))) } // CanCreateBranch returns true if repository is editable and user has proper access level. @@ -155,7 +156,7 @@ func RepoMustNotBeArchived() func(ctx *Context) { // MustEnableEditor checks if the web editor can be enabled for this repository func MustEnableEditor() func(ctx *Context) { return func(ctx *Context) { - if !ctx.Repo.CanEnableEditor() { + if !ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { ctx.NotFound(nil) } } @@ -167,13 +168,6 @@ func MustBeAbleToUpload() func(ctx *Context) { if !setting.Repository.Upload.Enabled || !ctx.Repo.Repository.CanEnableEditor() { ctx.NotFound(nil) } - } -} - -// MustHaveEditableRepository checks that there exists a repository that can be written -// to by the user for editing. -func MustHaveEditableRepository() func(ctx *Context) { - return func(ctx *Context) { editRepo, _, _ := GetEditableRepository(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName) if editRepo == nil { ctx.NotFound(nil) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index fa96d2f0e217c..5133d8797efd4 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -148,7 +148,7 @@ <a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> {{else}} <a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} + {{if and $.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} <a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> {{end}} {{end}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index c1975a73a624b..c29256651eebc 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -26,7 +26,7 @@ <h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3> <div class="repo-button-row"> - {{if and .CanWriteCode .Repository.CanEnableEditor}} + {{if and .CanWriteCode .CanEnableEditor}} <a class="ui small button" href="{{.RepoLink}}/_new/{{.BranchName | PathEscapeSegments}}/"> {{ctx.Locale.Tr "repo.editor.new_file"}} </a> diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 303b1ab9b3e56..ff122ca065d53 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,7 +41,7 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .RefFullName.IsBranch (not .IsViewFile) .Repository.CanEnableEditor .IsSigned}} + {{if and .RefFullName.IsBranch (not .IsViewFile) .CanEnableEditor .IsSigned}} <button class="ui dropdown basic compact jump button"> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index a5f199de67b69..c143333833f11 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -61,7 +61,7 @@ {{svg "octicon-rss"}} </a> {{end}} - {{if .Repository.CanEnableEditor}} + {{if .CanEnableEditor}} {{if .CanEditFile}} <a class="btn-octicon" data-tooltip-content="{{.EditFileTooltip}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}">{{svg "octicon-pencil"}}</a> {{else}} @@ -77,7 +77,7 @@ <button class="ui mini basic button unescape-button tw-mr-1 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> <button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> {{end}} - {{if and .ReadmeInList .Repository.CanEnableEditor}} + {{if and .ReadmeInList .CanEnableEditor}} {{if .CanEditReadmeFile}} <a class="btn-octicon" data-tooltip-content="{{.EditReadmeFileTooltip}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .FileTreePath}}">{{svg "octicon-pencil"}}</a> {{else}}