From ecff26de90736c9bef568a0c6a33df2034dc49ab Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 19 Jun 2025 20:28:09 +0800 Subject: [PATCH 1/3] fix --- options/locale/locale_en-US.ini | 7 +- routers/web/repo/cherry_pick.go | 193 ----- routers/web/repo/editor.go | 847 ++++--------------- routers/web/repo/editor_apply_patch.go | 74 ++ routers/web/repo/editor_cherry_pick.go | 111 +++ routers/web/repo/editor_error.go | 78 ++ routers/web/repo/editor_preview.go | 44 + routers/web/repo/editor_test.go | 79 +- routers/web/repo/editor_uploader.go | 67 ++ routers/web/repo/editor_util.go | 95 +++ routers/web/repo/patch.go | 126 --- services/context/repo.go | 34 +- services/context/upload/upload.go | 2 + services/repository/files/content.go | 4 +- services/repository/files/file.go | 7 +- services/repository/files/file_test.go | 18 +- services/repository/files/update.go | 4 +- services/repository/files/upload.go | 4 + templates/repo/editor/cherry_pick.tmpl | 9 +- templates/repo/editor/commit_form.tmpl | 24 +- templates/repo/editor/delete.tmpl | 2 +- templates/repo/editor/edit.tmpl | 5 +- templates/repo/editor/patch.tmpl | 5 +- templates/repo/editor/upload.tmpl | 4 +- tests/integration/api_repo_languages_test.go | 5 +- tests/integration/api_repo_license_test.go | 6 +- tests/integration/editor_test.go | 25 +- tests/integration/empty_repo_test.go | 10 +- tests/integration/pull_compare_test.go | 3 +- web_src/js/features/common-fetch-action.ts | 2 +- web_src/js/features/repo-editor.ts | 28 +- 31 files changed, 774 insertions(+), 1148 deletions(-) delete mode 100644 routers/web/repo/cherry_pick.go create mode 100644 routers/web/repo/editor_apply_patch.go create mode 100644 routers/web/repo/editor_cherry_pick.go create mode 100644 routers/web/repo/editor_error.go create mode 100644 routers/web/repo/editor_preview.go create mode 100644 routers/web/repo/editor_uploader.go create mode 100644 routers/web/repo/editor_util.go delete mode 100644 routers/web/repo/patch.go diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6d8aaef4cd85e..88ebe9065b3e8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1374,8 +1374,7 @@ editor.branch_already_exists = Branch "%s" already exists in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor` editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository. -editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository. -editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. +editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. editor.file_already_exists = A file named "%s" already exists in this repository. editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge. @@ -1383,8 +1382,6 @@ editor.push_out_of_date = The push appears to be out of date. editor.commit_empty_file_header = Commit an empty file editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. -editor.fail_to_update_file = Failed to update/create file "%s". -editor.fail_to_update_file_summary = Error Message: editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks. editor.push_rejected = The change was rejected by the server. Please check Git Hooks. editor.push_rejected_summary = Full Rejection Message: @@ -1398,6 +1395,8 @@ 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.failed_to_commit = Failed to commit changes. +editor.failed_to_commit_summary = Error Message: 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 deleted file mode 100644 index 690b830bc2f2a..0000000000000 --- a/routers/web/repo/cherry_pick.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "bytes" - "errors" - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "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" - "code.gitea.io/gitea/services/repository/files" -) - -var tplCherryPick templates.TplName = "repo/editor/cherry_pick" - -// CherryPick handles cherrypick GETs -func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam("sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - return - } - ctx.ServerError("GetCommit", err) - return - } - - if ctx.FormString("cherry-pick-type") == "revert" { - ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") - ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) - ctx.Data["commit_summary"] = splits[0] - ctx.Data["commit_message"] = splits[1] - } - - canCommit := renderCommitRights(ctx) - ctx.Data["TreePath"] = "" - - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - 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() - - ctx.HTML(http.StatusOK, tplCherryPick) -} - -// CherryPickPost handles cherrypick POSTs -func CherryPickPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CherryPickForm) - - sha := ctx.PathParam("sha") - ctx.Data["SHA"] = sha - if form.Revert { - ctx.Data["CherryPickType"] = "revert" - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - } - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - 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() - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplCherryPick) - return - } - - // Cannot commit to a 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), tplCherryPick, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if message == "" { - if form.Revert { - message = ctx.Locale.TrString("repo.commit.revert-header", sha) - } else { - message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha) - } - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form) - return - } - opts := &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Author: gitCommitter, - Committer: gitCommitter, - } - - // First lets try the simple plain read-tree -m approach - opts.Content = sha - if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - // Drop through to the apply technique - - buf := &bytes.Buffer{} - if form.Revert { - if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } else { - if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } - - opts.Content = buf.String() - ctx.Data["FileContent"] = opts.Content - - if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - } - - 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)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) - } -} diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 1a090c94374d7..5f18f6b1f7f8b 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -4,6 +4,7 @@ package repo import ( + "bytes" "fmt" "io" "net/http" @@ -11,18 +12,15 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" - 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/json" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" @@ -34,141 +32,146 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" + tplPatchFile templates.TplName = "repo/editor/patch" + tplCherryPick templates.TplName = "repo/editor/cherry_pick" - frmCommitChoiceDirect string = "direct" - frmCommitChoiceNewBranch string = "commit-to-new-branch" + editorCommitChoiceDirect string = "direct" + editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func canCreateBasePullRequest(ctx *context.Context) bool { - baseRepo := ctx.Repo.Repository.BaseRepo - return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests) +type EditorCommitFormOptions struct { + CommitFormBehaviors *context.CommitFormBehaviors } -func renderCommitRights(ctx *context.Context) bool { - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) +func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *EditorCommitFormOptions { + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + queryFilename := ctx.Req.URL.Query().Get("filename") + if queryFilename != "" { + newTreePath := path.Join(ctx.Repo.TreePath, queryFilename) + ctx.Redirect(fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))) + return nil + } + + cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if cleanedTreePath != ctx.Repo.TreePath { + ctx.Redirect(fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))) + return nil + } + ctx.Repo.TreePath = cleanedTreePath + + commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer) if err != nil { - log.Error("CanCommitToBranch: %v", err) + ctx.ServerError("PrepareCommitFormBehaviors", err) + return nil } - ctx.Data["CanCommitToBranch"] = canCommitToBranch - ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx) + opts := &EditorCommitFormOptions{ + CommitFormBehaviors: commitFormBehaviors, + } + + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["TreePath"] = ctx.Repo.TreePath + ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(ctx.Repo.TreePath) + ctx.Data["CommitFormBehaviors"] = commitFormBehaviors + ctx.Data["CommitFormOptions"] = opts + + // for online editor + 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") - return canCommitToBranch.CanCommitToBranch + ctx.Data["commit_summary"] = "" + ctx.Data["commit_message"] = "" + ctx.Data["commit_choice"] = util.Iif(opts.CommitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) + ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository) + ctx.Data["last_commit"] = ctx.Repo.CommitID + + return opts } // redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) { - if commitChoice == frmCommitChoiceNewBranch { +func redirectForCommitChoice(ctx *context.Context, formOpts *EditorCommitFormOptions, commitChoice, newBranchName, treePath string) { + if commitChoice == editorCommitChoiceNewBranch { // Redirect to a pull request when possible redirectToPullRequest := false - repo := ctx.Repo.Repository - baseBranch := ctx.Repo.BranchName - headBranch := newBranchName + repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, newBranchName if repo.UnitEnabled(ctx, unit.TypePullRequests) { redirectToPullRequest = true - } else if canCreateBasePullRequest(ctx) { + } else if formOpts.CommitFormBehaviors.CanCreateBasePullRequest { redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo } - if redirectToPullRequest { - ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) + ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) return } } returnURI := ctx.FormString("return_uri") - - ctx.RedirectToCurrentSite( - returnURI, - ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), - ) + if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { + returnURI = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath) + } + ctx.JSONRedirect(returnURI) } -// getParentTreeFields returns list of parent tree names and corresponding tree paths -// based on given tree path. -func getParentTreeFields(treePath string) (treeNames, treePaths []string) { - if len(treePath) == 0 { - return treeNames, treePaths +func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "GetTreeEntryByPath", err) + return nil, nil, nil } - treeNames = strings.Split(treePath, "/") - treePaths = make([]string, len(treeNames)) - for i := range treeNames { - treePaths[i] = strings.Join(treeNames[:i+1], "/") + // No way to edit a directory online. + if entry.IsDir() { + ctx.NotFound(nil) + return nil, nil, nil } - return treeNames, treePaths -} - -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) { - editFileCommon(ctx, isNewFile) - canCommit := renderCommitRights(ctx) - - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - if isNewFile { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + blob := entry.Blob() + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) } else { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + ctx.ServerError("getFileReader", err) } - return + return nil, nil, nil } - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) - - if !isNewFile { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if fInfo.isLFSFile { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - // No way to edit a directory online. - if entry.IsDir() { + _ = dataRc.Close() + ctx.ServerError("GetTreePathLock", err) + return nil, nil, nil + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + _ = dataRc.Close() ctx.NotFound(nil) - return + return nil, nil, nil } + } - blob := entry.Blob() + return buf, dataRc, fInfo +} - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - } else { - ctx.ServerError("getFileReader", err) - } - return - } +func editFile(ctx *context.Context, editorAction string) { + isNewFile := editorAction == "_new" + ctx.Data["IsNewFile"] = isNewFile - defer dataRc.Close() + _ = prepareEditorCommitFormOptions(ctx, editorAction) + if ctx.Written() { + return + } - if fInfo.isLFSFile { - lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.NotFound(nil) - return - } + if !isNewFile { + prefetch, dataRc, fInfo := editFileOpenExisting(ctx) + if ctx.Written() { + return } + defer dataRc.Close() ctx.Data["FileSize"] = fInfo.fileSize @@ -179,112 +182,62 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") - } else { - d, _ := io.ReadAll(dataRc) + } - buf = append(buf, d...) + if ctx.Data["NotEditableReason"] == nil { + buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc)) + if err != nil { + ctx.ServerError("ReadAll", err) + return + } if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) ctx.Data["FileContent"] = string(buf) } else { ctx.Data["FileContent"] = content } } - } else { - // Append filename from query, or empty string to allow username the new file. - treeNames = append(treeNames, fileName) } - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - 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["last_commit"] = ctx.Repo.CommitID - - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - + ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplEditFile) } -// GetEditorConfig returns a editorconfig JSON string for given treePath or "null" -func GetEditorConfig(ctx *context.Context, treePath string) string { - ec, _, err := ctx.Repo.GetEditorconfig() - if err == nil { - def, err := ec.GetDefinitionForFilename(treePath) - if err == nil { - jsonStr, _ := json.Marshal(def) - return string(jsonStr) - } - } - return "null" -} - // EditFile render edit file page func EditFile(ctx *context.Context) { - editFile(ctx, false) + editFile(ctx, "_edit") } // NewFile render create file page func NewFile(ctx *context.Context) { - editFile(ctx, true) + editFile(ctx, "_new") } -func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editFileCommon(ctx, isNewFile) - ctx.Data["PageHasPosted"] = true - - canCommit := renderCommitRights(ctx) - treeNames, treePaths := getParentTreeFields(form.TreePath) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) - +func editFilePost(ctx *context.Context, form *forms.EditRepoFileForm, editorAction string) { + form.TreePath = files_service.CleanGitTreePath(form.TreePath) if ctx.HasError() { - ctx.HTML(http.StatusOK, tplEditFile) + ctx.JSONError(ctx.GetErrMsg()) return } - // 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) + isNewFile := editorAction == "_new" + formOpts := prepareEditorCommitFormOptions(ctx, editorAction) + if ctx.Written() { return } - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - if isNewFile { - message = ctx.Locale.TrString("repo.editor.add", form.TreePath) - } else { - message = ctx.Locale.TrString("repo.editor.update", form.TreePath) - } - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage + branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName) + if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName)) + return } + defaultMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", form.TreePath), ctx.Locale.TrString("repo.editor.update", form.TreePath)) + commitMessage := buildEditorCommitMessage(defaultMessage, form.CommitSummary, form.CommitMessage) + + // Committer user info gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form) + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) return } @@ -299,16 +252,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "rename" } else { // It should never happen, just in case - ctx.Flash.Error(ctx.Tr("error.occurred")) - ctx.HTML(http.StatusOK, tplEditFile) + ctx.JSONError(ctx.Tr("error.occurred")) return } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, - Message: message, + Message: commitMessage, Files: []*files_service.ChangeRepoFile{ { Operation: operation, @@ -320,204 +272,58 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b Signoff: form.Signoff, Author: gitCommitter, Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by files_service.ChangeRepoFiles - if git.IsErrNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form) - } else if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form) - default: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form) - } - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) - } else if git.IsErrBranchNotExist(err) { - // For when a user adds/updates a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), - "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), - "Details": utils.SanitizeFlashErrorString(err.Error()), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } + }) + if err != nil { + editorHandleFileOperationError(ctx, branchName, err) + return } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) + redirectForCommitChoice(ctx, formOpts, form.CommitChoice, branchName, form.TreePath) } -// EditFilePost response for editing file func EditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, false) + editFilePost(ctx, web.GetForm(ctx).(*forms.EditRepoFileForm), "_edit") } -// NewFilePost response for creating file func NewFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, true) -} - -// DiffPreviewPost render preview diff page -func DiffPreviewPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditPreviewDiffForm) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if len(treePath) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid") - return - } - - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error()) - return - } else if entry.IsDir() { - ctx.HTTPError(http.StatusUnprocessableEntity) - return - } - - diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error()) - return - } - - if len(diff.Files) != 0 { - ctx.Data["File"] = diff.Files[0] - } - - ctx.HTML(http.StatusOK, tplEditDiffPreview) + editFilePost(ctx, web.GetForm(ctx).(*forms.EditRepoFileForm), "_new") } // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treePath := cleanUploadFileName(ctx.Repo.TreePath) - - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + _ = prepareEditorCommitFormOptions(ctx, "_delete") + if ctx.Written() { return } - - ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx) - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["last_commit"] = ctx.Repo.CommitID - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - + ctx.Data["PageIsDelete"] = true ctx.HTML(http.StatusOK, tplDeleteFile) } // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplDeleteFile) + ctx.JSONError(ctx.GetErrMsg()) 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) + formOpts := prepareEditorCommitFormOptions(ctx, "_delete") + + branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName) + if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName)) return } - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath) - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } + commitMessage := buildEditorCommitMessage(ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath), form.CommitSummary, form.CommitMessage) gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form) + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) return } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, @@ -527,392 +333,103 @@ func DeleteFilePost(ctx *context.Context) { TreePath: ctx.Repo.TreePath, }, }, - Message: message, + Message: commitMessage, Signoff: form.Signoff, Author: gitCommitter, Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by repofiles.DeleteRepoFile - if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form) - default: - ctx.ServerError("DeleteRepoFile", err) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } - } else if git.IsErrBranchNotExist(err) { - // For when a user deletes a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("DeleteFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplDeleteFile, &form) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } + }) + if err != nil { + editorHandleFileOperationError(ctx, branchName, err) return } ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath)) - treePath := path.Dir(ctx.Repo.TreePath) - if treePath == "." { - treePath = "" // the file deleted was in the root, so we return the user to the root directory - } - if len(treePath) > 0 { - // Need to get the latest commit since it changed - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - if err == nil && commit != nil { - // We have the comment, now find what directory we can return the user to - // (must have entries) - treePath = GetClosestParentWithFiles(treePath, commit) - } else { - treePath = "" // otherwise return them to the root of the repo - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath) + redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, ctx.Repo.BranchName, ctx.Repo.TreePath) + redirectForCommitChoice(ctx, formOpts, form.CommitChoice, branchName, redirectTreePath) } // UploadFile render upload file page func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - 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))) + _ = prepareEditorCommitFormOptions(ctx, "_upload") + if ctx.Written() { return } - ctx.Repo.TreePath = treePath - - treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.HTML(http.StatusOK, tplUploadFile) } // UploadFilePost response for uploading file func UploadFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.UploadRepoFileForm) ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - oldBranchName := ctx.Repo.BranchName - branchName := oldBranchName - - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName + form := web.GetForm(ctx).(*forms.UploadRepoFileForm) + form.TreePath = files_service.CleanGitTreePath(form.TreePath) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return } - form.TreePath = cleanUploadFileName(form.TreePath) - - treeNames, treePaths := getParentTreeFields(form.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} + formOpts := prepareEditorCommitFormOptions(ctx, "_upload") + if ctx.Written() { + return } - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = branchName + oldBranchName := ctx.Repo.BranchName - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplUploadFile) - return - } + branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName) 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) + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", branchName)) 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) + } else if !formOpts.CommitFormBehaviors.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName)) return } - if !ctx.Repo.Repository.IsEmpty { - var newTreePath string - for _, part := range treeNames { - newTreePath = path.Join(newTreePath, part) - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) - if err != nil { - if git.IsErrNotExist(err) { - break // Means there is no item with that name, so we're good - } - ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) + if !ctx.Repo.Repository.IsEmpty && form.TreePath != "" { + _, treePaths := getParentTreeFields(form.TreePath) + for _, parentTreePath := range treePaths { + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(parentTreePath) + if git.IsErrNotExist(err) { + break // Means there is no item with that name, so we're good + } else if err != nil { + ctx.ServerError("GetTreeEntryByPath", err) return } - // User can only upload files to a directory, the directory name shouldn't be an existing file. if !entry.IsDir() { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) + ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", parentTreePath)) return } } } - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - dir := form.TreePath - if dir == "" { - dir = "/" - } - message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir) - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } + commitMessage := buildEditorCommitMessage(ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(form.TreePath, "/")), form.CommitSummary, form.CommitMessage) gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form) + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) return } - if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ + err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ LastCommitID: ctx.Repo.CommitID, OldBranch: oldBranchName, NewBranch: branchName, TreePath: form.TreePath, - Message: message, + Message: commitMessage, Files: form.Files, Signoff: form.Signoff, Author: gitCommitter, Committer: gitCommitter, - }); err != nil { - if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - fileErr := err.(files_service.ErrFilePathInvalid) - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form) - default: - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form) - } else if git.IsErrBranchNotExist(err) { - branchErr := err.(git.ErrBranchNotExist) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("UploadFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplUploadFile, &form) - } - } 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) - 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.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -func cleanUploadFileName(name string) string { - // Rebase the filename - name = util.PathJoinRel(name) - // Git disallows any filenames to have a .git directory in them. - for part := range strings.SplitSeq(name, "/") { - if strings.ToLower(part) == ".git" { - return "" - } - } - return name -} - -// UploadFileToServer upload file to server file dir not git -func UploadFileToServer(ctx *context.Context) { - file, header, err := ctx.Req.FormFile("file") - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) - return - } - defer file.Close() - - buf := make([]byte, 1024) - n, _ := util.ReadAtMost(file, buf) - if n > 0 { - buf = buf[:n] - } - - err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) - if err != nil { - ctx.HTTPError(http.StatusBadRequest, err.Error()) - return - } - - name := cleanUploadFileName(header.Filename) - if len(name) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid") - return - } - - upload, err := repo_model.NewUpload(ctx, name, buf, file) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err)) - return - } - - log.Trace("New file uploaded: %s", upload.UUID) - ctx.JSON(http.StatusOK, map[string]string{ - "uuid": upload.UUID, }) -} - -// RemoveUploadFileFromServer remove file from server file dir -func RemoveUploadFileFromServer(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) - if len(form.File) == 0 { - ctx.Status(http.StatusNoContent) - return - } - - if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + if err != nil { + editorHandleFileOperationError(ctx, branchName, err) return } - log.Trace("Upload file removed: %s", form.File) - ctx.Status(http.StatusNoContent) -} - -// GetUniquePatchBranchName Gets a unique branch name for a new patch branch -// It will be in the form of -patch- where 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 { - 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 { - log.Error("GetUniquePatchBranchName: %v", err) - return "" - } else if !exist { - return branchName - } - } - return "" -} - -// 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. -func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { - if len(treePath) == 0 || treePath == "." { - return "" - } - // see if the tree has entries - if tree, err := commit.SubTree(treePath); err != nil { - // failed to get tree, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { - // no files in this dir, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } - return treePath + redirectForCommitChoice(ctx, formOpts, form.CommitChoice, branchName, form.TreePath) } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go new file mode 100644 index 0000000000000..f8026f7188b53 --- /dev/null +++ b/routers/web/repo/editor_apply_patch.go @@ -0,0 +1,74 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func NewDiffPatch(ctx *context.Context) { + _ = prepareEditorCommitFormOptions(ctx, "_diffpatch") + if ctx.Written() { + return + } + ctx.Data["PageIsPatch"] = true + ctx.HTML(http.StatusOK, tplPatchFile) +} + +// NewDiffPatchPost response for sending patch page +func NewDiffPatchPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditRepoFileForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + formOpts := prepareEditorCommitFormOptions(ctx, "_diffpatch") + if ctx.Written() { + return + } + + branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName) + if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName)) + return + } + + commitMessage := buildEditorCommitMessage(ctx.Locale.TrString("repo.editor.patch"), form.CommitSummary, form.CommitMessage) + + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) + return + } + + fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ + LastCommitID: form.LastCommit, + OldBranch: ctx.Repo.BranchName, + NewBranch: branchName, + Message: commitMessage, + Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), + Author: gitCommitter, + Committer: gitCommitter, + }) + if err != nil { + editorHandleFileOperationError(ctx, branchName, err) + return + } + + if form.CommitChoice == editorCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { + ctx.JSONRedirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) + } else { + ctx.JSONRedirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) + } +} diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go new file mode 100644 index 0000000000000..5aa2f96a6f9e0 --- /dev/null +++ b/routers/web/repo/editor_cherry_pick.go @@ -0,0 +1,111 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "net/http" + "strings" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func CherryPick(ctx *context.Context) { + _ = prepareEditorCommitFormOptions(ctx, "_cherrypick") + if ctx.Written() { + return + } + + fromCommitID := ctx.PathParam("sha") + ctx.Data["FromCommitID"] = fromCommitID + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID) + if err != nil { + HandleGitError(ctx, "GetCommit", err) + return + } + + if ctx.FormString("cherry-pick-type") == "revert" { + ctx.Data["CherryPickType"] = "revert" + ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") + ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() + } else { + ctx.Data["CherryPickType"] = "cherry-pick" + splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) + ctx.Data["commit_summary"] = splits[0] + ctx.Data["commit_message"] = splits[1] + } + + ctx.HTML(http.StatusOK, tplCherryPick) +} + +func CherryPickPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CherryPickForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + formOpts := prepareEditorCommitFormOptions(ctx, "_cherrypick") + if ctx.Written() { + return + } + + fromCommitID := ctx.PathParam("sha") + + branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName) + if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName)) + return + } + + defaultMessage := util.Iif(form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID)) + commitMessage := buildEditorCommitMessage(defaultMessage, form.CommitSummary, form.CommitMessage) + + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) + return + } + + opts := &files.ApplyDiffPatchOptions{ + LastCommitID: form.LastCommit, + OldBranch: ctx.Repo.BranchName, + NewBranch: branchName, + Message: commitMessage, + Author: gitCommitter, + Committer: gitCommitter, + } + + // First try the simple plain read-tree -m approach + opts.Content = fromCommitID + if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { + // Drop through to the "apply" method + buf := &bytes.Buffer{} + if form.Revert { + err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf) + } else { + err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf) + } + if err == nil { + opts.Content = buf.String() + _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) + } + if err != nil { + editorHandleFileOperationError(ctx, branchName, err) + return + } + } + + if form.CommitChoice == editorCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { + ctx.JSONRedirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) + } else { + ctx.JSONRedirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) + } +} diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go new file mode 100644 index 0000000000000..85f8d6d1e3c27 --- /dev/null +++ b/routers/web/repo/editor_error.go @@ -0,0 +1,78 @@ +// Copyright 2025 Gitea Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/utils" + context_service "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func errorAs[T error](v error) (e T, ok bool) { + if errors.As(v, &e) { + return e, true + } + return e, false +} + +func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) { + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ + "Message": message, + "Summary": summary, + "Details": utils.SanitizeFlashErrorString(details), + }) + if err == nil { + ctx.JSONError(flashError) + } else { + log.Error("RenderToHTML: %v", err) + ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + } +} + +func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) { + if git.IsErrNotExist(err) { + ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", ctx.Repo.TreePath)) + } else if git_model.IsErrLFSFileLocked(err) { + ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName)) + } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](errAs); ok { + switch errAs.Type { + case git.EntryModeSymlink: + ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path)) + case git.EntryModeTree: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path)) + case git.EntryModeBlob: + ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path)) + default: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } + } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](errAs); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path)) + } else if errAs, ok := errorAs[git.ErrBranchNotExist](errAs); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name)) + } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](errAs); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName)) + } else if files_service.IsErrCommitIDDoesNotMatch(errAs) { + ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching")) + } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { + ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName))) + } else if errAs, ok := errorAs[*git.ErrPushRejected](errAs); ok { + if errAs.Message == "" { + ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message")) + } else { + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message) + } + } else { + setting.PanicInDevOrTesting("unclear err: %v", err) + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error()) + } +} diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go new file mode 100644 index 0000000000000..771e4000aad10 --- /dev/null +++ b/routers/web/repo/editor_preview.go @@ -0,0 +1,44 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// DiffPreviewPost render preview diff page +func DiffPreviewPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditPreviewDiffForm) + treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if treePath == "" { + ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid") + return + } + + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.ServerError("GetTreeEntryByPath", err) + return + } else if entry.IsDir() { + ctx.HTTPError(http.StatusUnprocessableEntity) + return + } + + diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) + if err != nil { + ctx.ServerError("GetDiffPreview", err) + return + } + + if len(diff.Files) != 0 { + ctx.Data["File"] = diff.Files[0] + } + + ctx.HTML(http.StatusOK, tplEditDiffPreview) +} diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 89bf8f309cd5e..6e2c1d62197f0 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -6,76 +6,27 @@ package repo import ( "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestCleanUploadName(t *testing.T) { +func TestEditorUtils(t *testing.T) { unittest.PrepareTestEnv(t) - - kases := map[string]string{ - ".git/refs/master": "", - "/root/abc": "root/abc", - "./../../abc": "abc", - "a/../.git": "", - "a/../../../abc": "abc", - "../../../acd": "acd", - "../../.git/abc": "", - "..\\..\\.git/abc": "..\\..\\.git/abc", - "..\\../.git/abc": "", - "..\\../.git": "", - "abc/../def": "def", - ".drone.yml": ".drone.yml", - ".abc/def/.drone.yml": ".abc/def/.drone.yml", - "..drone.yml.": "..drone.yml.", - "..a.dotty...name...": "..a.dotty...name...", - "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...", - } - for k, v := range kases { - assert.Equal(t, cleanUploadFileName(k), v) - } -} - -func TestGetUniquePatchBranchName(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - expectedBranchName := "user2-patch-1" - branchName := GetUniquePatchBranchName(ctx) - assert.Equal(t, expectedBranchName, branchName) -} - -func TestGetClosestParentWithFiles(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - branch := repo.DefaultBranch - gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) - defer gitRepo.Close() - commit, _ := gitRepo.GetBranchCommit(branch) - var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo - for _, deletedFile := range []string{ - "dir1/dir2/dir3/file.txt", - "file.txt", - } { - treePath := GetClosestParentWithFiles(deletedFile, commit) - assert.Equal(t, expectedTreePath, treePath) - } + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + t.Run("getUniquePatchBranchName", func(t *testing.T) { + branchName := getUniquePatchBranchName(t.Context(), "user2", repo) + assert.Equal(t, "user2-patch-1", branchName) + }) + t.Run("getClosestParentWithFiles", func(t *testing.T) { + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar") + assert.Equal(t, "docs", treePath) + treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other") + assert.Empty(t, treePath) + }) } diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go new file mode 100644 index 0000000000000..6ab7a739cd094 --- /dev/null +++ b/routers/web/repo/editor_uploader.go @@ -0,0 +1,67 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/context/upload" + "code.gitea.io/gitea/services/forms" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// UploadFileToServer upload file to server file dir not git +func UploadFileToServer(ctx *context.Context) { + file, header, err := ctx.Req.FormFile("file") + if err != nil { + ctx.ServerError("FormFile", err) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := util.ReadAtMost(file, buf) + if n > 0 { + buf = buf[:n] + } + + err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) + if err != nil { + ctx.HTTPError(http.StatusBadRequest, err.Error()) + return + } + + name := files_service.CleanGitTreePath(header.Filename) + if len(name) == 0 { + ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid") + return + } + + uploaded, err := repo_model.NewUpload(ctx, name, buf, file) + if err != nil { + ctx.ServerError("NewUpload", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID}) +} + +// RemoveUploadFileFromServer remove file from server file dir +func RemoveUploadFileFromServer(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) + if form.File == "" { + ctx.Status(http.StatusNoContent) + return + } + if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil { + ctx.ServerError("DeleteUploadByUUID", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go new file mode 100644 index 0000000000000..94ce492269f05 --- /dev/null +++ b/routers/web/repo/editor_util.go @@ -0,0 +1,95 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "path" + "strings" + + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + context_service "code.gitea.io/gitea/services/context" +) + +// getUniquePatchBranchName Gets a unique branch name for a new patch branch +// It will be in the form of -patch- where 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, prefixName string, repo *repo_model.Repository) string { + prefix := prefixName + "-patch-" + for i := 1; i <= 1000; i++ { + branchName := fmt.Sprintf("%s%d", prefix, i) + if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil { + log.Error("getUniquePatchBranchName: %v", err) + return "" + } else if !exist { + return branchName + } + } + return "" +} + +// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is +// deleted. It returns "" for the tree root if no parents other than the root have files. +func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string { + var f func(treePath string, commit *git.Commit) string + f = func(treePath string, commit *git.Commit) string { + if treePath == "" || treePath == "." { + return "" + } + // see if the tree has entries + if tree, err := commit.SubTree(treePath); err != nil { + return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir + } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { + return f(path.Dir(treePath), commit) // no files in this dir, going up a dir + } + return treePath + } + commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change + if err != nil { + log.Error("GetBranchCommit: %v", err) + return "" + } + return f(originTreePath, commit) +} + +// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null" +func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string { + ec, _, err := ctx.Repo.GetEditorconfig() + if err == nil { + def, err := ec.GetDefinitionForFilename(treePath) + if err == nil { + jsonStr, _ := json.Marshal(def) + return string(jsonStr) + } + } + return "null" +} + +// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath. +// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"} +// or: []{""}, []{""} for the root treePath +func getParentTreeFields(treePath string) (treeNames, treePaths []string) { + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +func buildEditorCommitMessage(def, summary, body string) string { + message := util.IfZero(strings.TrimSpace(summary), def) + body = strings.TrimSpace(body) + if body != "" { + message += "\n\n" + body + } + return message +} diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go deleted file mode 100644 index 3ffd8f89c4e20..0000000000000 --- a/routers/web/repo/patch.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/setting" - "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" - "code.gitea.io/gitea/services/repository/files" -) - -const ( - tplPatchFile templates.TplName = "repo/editor/patch" -) - -// NewDiffPatch render create patch page -func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx) - - ctx.Data["PageIsPatch"] = true - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - 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() - - ctx.HTML(http.StatusOK, tplPatchFile) -} - -// NewDiffPatchPost response for sending patch page -func NewDiffPatchPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplPatchFile) - return - } - - // 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) - return - } - - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.patch") - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) - return - } - - fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), - Author: gitCommitter, - Committer: gitCommitter, - }) - if err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - - 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)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) - } -} diff --git a/services/context/repo.go b/services/context/repo.go index 32d54c88ff8de..c28ae7e8fd786 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -94,24 +94,22 @@ func RepoMustNotBeArchived() func(ctx *Context) { } } -// CanCommitToBranchResults represents the results of CanCommitToBranch -type CanCommitToBranchResults struct { - CanCommitToBranch bool - EditorEnabled bool - UserCanPush bool - RequireSigned bool - WillSign bool - SigningKey *git.SigningKey - WontSignReason string +type CommitFormBehaviors struct { + CanCommitToBranch bool + EditorEnabled bool + UserCanPush bool + RequireSigned bool + WillSign bool + SigningKey *git.SigningKey + WontSignReason string + CanCreatePullRequest bool + CanCreateBasePullRequest bool } -// 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) { +func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) { protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) if err != nil { - return CanCommitToBranchResults{}, err + return nil, err } userCanPush := true requireSigned := false @@ -138,7 +136,10 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use } } - return CanCommitToBranchResults{ + canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) + canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest + + return &CommitFormBehaviors{ CanCommitToBranch: canCommit, EditorEnabled: canEnableEditor, UserCanPush: userCanPush, @@ -146,6 +147,9 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use WillSign: sign, SigningKey: keyID, WontSignReason: wontSignReason, + + CanCreatePullRequest: canCreatePullRequest, + CanCreateBasePullRequest: canCreateBasePullRequest, }, err } diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 5edddc6f27f5c..303e7da38b113 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -113,5 +113,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + default: + setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType) } } diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 7a07a0ddca431..ccba3b759478d 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -42,7 +42,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refComm } // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) + cleanTreePath := CleanGitTreePath(treePath) if cleanTreePath == "" && treePath != "" { return nil, ErrFilenameInvalid{ Path: treePath, @@ -103,7 +103,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType { // GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) { // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) + cleanTreePath := CleanGitTreePath(treePath) if cleanTreePath == "" && treePath != "" { return nil, ErrFilenameInvalid{ Path: treePath, diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 0e1100a098b2f..6b9746a57934e 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -134,8 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error { return util.ErrInvalidArgument } -// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory -func CleanUploadFileName(name string) string { +// CleanGitTreePath Trims a filename and returns empty string if it is a .git directory +func CleanGitTreePath(name string) string { // Rebase the filename name = util.PathJoinRel(name) // Git disallows any filenames to have a .git directory in them. @@ -144,5 +144,8 @@ func CleanUploadFileName(name string) string { return "" } } + if name == "." { + name = "" + } return name } diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index 169cafba0db1d..894c184472e6e 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -10,17 +10,9 @@ import ( ) func TestCleanUploadFileName(t *testing.T) { - t.Run("Clean regular file", func(t *testing.T) { - name := "this/is/test" - cleanName := CleanUploadFileName(name) - expectedCleanName := name - assert.Equal(t, expectedCleanName, cleanName) - }) - - t.Run("Clean a .git path", func(t *testing.T) { - name := "this/is/test/.git" - cleanName := CleanUploadFileName(name) - expectedCleanName := "" - assert.Equal(t, expectedCleanName, cleanName) - }) + assert.Equal(t, "", CleanGitTreePath("")) //nolint + assert.Equal(t, "", CleanGitTreePath(".")) //nolint + assert.Equal(t, "a/b", CleanGitTreePath("a/b")) + assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint + assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 99c1215c9ffe8..3a57e4b141a00 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -127,14 +127,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Check that the path given in opts.treePath is valid (not a git path) - treePath := CleanUploadFileName(file.TreePath) + treePath := CleanGitTreePath(file.TreePath) if treePath == "" { return nil, ErrFilenameInvalid{ Path: file.TreePath, } } // If there is a fromTreePath (we are copying it), also clean it up - fromTreePath := CleanUploadFileName(file.FromTreePath) + fromTreePath := CleanGitTreePath(file.FromTreePath) if fromTreePath == "" && file.FromTreePath != "" { return nil, ErrFilenameInvalid{ Path: file.FromTreePath, diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index b004e3cc4cd6d..cd1951ccaf0b3 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -180,6 +180,10 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return err } + if repo.IsEmpty { + _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty") + } + return repo_model.DeleteUploads(ctx, uploads...) } diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index f9c9eef5aab64..e5d1e2e5eedbf 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -3,15 +3,14 @@ {{template "repo/header" .}}
{{template "base/alert" .}} -
+ {{.CsrfTokenHtml}} -
-