1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-28 13:18:37 +00:00

Refactor editor (#34780)

A complete rewrite
This commit is contained in:
wxiaoguang
2025-06-21 19:20:51 +08:00
committed by GitHub
parent 81adb01713
commit 4fc626daa1
41 changed files with 977 additions and 1638 deletions

View File

@@ -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))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"strings"
"code.gitea.io/gitea/modules/util"
"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) {
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
if ctx.Written() {
return
}
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
})
if err != nil {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
}
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
return
}
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}

View File

@@ -0,0 +1,86 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
"net/http"
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
"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) {
fromCommitID := ctx.PathParam("sha")
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
if ctx.Written() {
return
}
defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
opts := &files.ApplyDiffPatchOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
}
// First try the simple plain read-tree -m approach
opts.Content = fromCommitID
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
// Drop through to the "apply" method
buf := &bytes.Buffer{}
if parsed.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 {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
}
}
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
return
}
}
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}

View File

@@ -0,0 +1,82 @@
// Copyright 2025 Gitea 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 errAs := util.ErrorAsLocale(err); errAs != nil {
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
} else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.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](err); 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](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
} else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
} else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
} else if files_service.IsErrCommitIDDoesNotMatch(err) {
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](err); 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 if errors.Is(err, util.ErrNotExist) {
ctx.JSONError(ctx.Tr("error.not_found"))
} else {
setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
)
func DiffPreviewPost(ctx *context.Context) {
content := ctx.FormString("content")
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, 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)
}

View File

@@ -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)
})
}

View File

@@ -0,0 +1,61 @@
// 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/services/context"
"code.gitea.io/gitea/services/context/upload"
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) {
fileUUID := ctx.FormString("file")
if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
ctx.ServerError("DeleteUploadByUUID", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@@ -0,0 +1,85 @@
// 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"
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 <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, 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
}

View File

@@ -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)
}
}