mirror of
https://github.com/go-gitea/gitea
synced 2025-07-28 13:18:37 +00:00
@@ -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
51
routers/web/repo/editor_apply_patch.go
Normal file
51
routers/web/repo/editor_apply_patch.go
Normal 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)
|
||||
}
|
86
routers/web/repo/editor_cherry_pick.go
Normal file
86
routers/web/repo/editor_cherry_pick.go
Normal 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)
|
||||
}
|
82
routers/web/repo/editor_error.go
Normal file
82
routers/web/repo/editor_error.go
Normal 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())
|
||||
}
|
||||
}
|
41
routers/web/repo/editor_preview.go
Normal file
41
routers/web/repo/editor_preview.go
Normal 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)
|
||||
}
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
61
routers/web/repo/editor_uploader.go
Normal file
61
routers/web/repo/editor_uploader.go
Normal 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)
|
||||
}
|
85
routers/web/repo/editor_util.go
Normal file
85
routers/web/repo/editor_util.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user