mirror of
https://github.com/go-gitea/gitea.git
synced 2025-06-21 22:07:55 +00:00
parent
81adb01713
commit
4fc626daa1
@ -112,7 +112,6 @@ type LFSMetaObject struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
lfs.Pointer `xorm:"extends"`
|
||||
RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
Existing bool `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if exist {
|
||||
m.Existing = true
|
||||
return m, committer.Commit()
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,8 @@ func (o *UpdateFileOptions) Branch() string {
|
||||
return o.FileOptions.BranchName
|
||||
}
|
||||
|
||||
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
|
||||
|
||||
// ChangeFileOperation for creating, updating or deleting a file
|
||||
type ChangeFileOperation struct {
|
||||
// indicates what to do with the file
|
||||
|
@ -1354,7 +1354,7 @@ editor.update = Update %s
|
||||
editor.delete = Delete %s
|
||||
editor.patch = Apply Patch
|
||||
editor.patching = Patching:
|
||||
editor.fail_to_apply_patch = Unable to apply patch "%s"
|
||||
editor.fail_to_apply_patch = Unable to apply patch
|
||||
editor.new_patch = New Patch
|
||||
editor.commit_message_desc = Add an optional extended description…
|
||||
editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message.
|
||||
@ -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. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> 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
|
||||
|
@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
// FIXME: actually now we support more operations like "rename", "upload"
|
||||
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options.
|
||||
// Need to fully fix them in API
|
||||
changeRepoFile := &files_service.ChangeRepoFile{
|
||||
Operation: file.Operation,
|
||||
TreePath: file.Path,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1315,11 +1315,11 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Group("/{username}/{reponame}", func() { // repo code
|
||||
m.Group("", func() {
|
||||
m.Group("", func() {
|
||||
m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
|
||||
m.Combo("/_edit/*").Get(repo.EditFile).
|
||||
m.Post("/_preview/*", repo.DiffPreviewPost)
|
||||
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/_new/*").Get(repo.NewFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
|
||||
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
||||
@ -1331,7 +1331,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
||||
m.Group("", func() {
|
||||
m.Post("/upload-file", repo.UploadFileToServer)
|
||||
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
|
||||
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
||||
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
|
||||
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@ -681,129 +680,6 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ___________ .___.__ __
|
||||
// \_ _____/ __| _/|__|/ |_
|
||||
// | __)_ / __ | | \ __\
|
||||
// | \/ /_/ | | || |
|
||||
// /_______ /\____ | |__||__|
|
||||
// \/ \/
|
||||
|
||||
// EditRepoFileForm form for changing repository file
|
||||
type EditRepoFileForm struct {
|
||||
TreePath string `binding:"Required;MaxSize(500)"`
|
||||
Content optional.Option[string]
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// EditPreviewDiffForm form for changing preview diff
|
||||
type EditPreviewDiffForm struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// _________ .__ __________.__ __
|
||||
// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __
|
||||
// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ /
|
||||
// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| <
|
||||
// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \
|
||||
// \/ \/ \/ \/ \/ \/
|
||||
|
||||
// CherryPickForm form for changing repository file
|
||||
type CherryPickForm struct {
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Revert bool
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ____ ___ .__ .___
|
||||
// | | \______ | | _________ __| _/
|
||||
// | | /\____ \| | / _ \__ \ / __ |
|
||||
// | | / | |_> > |_( <_> ) __ \_/ /_/ |
|
||||
// |______/ | __/|____/\____(____ /\____ |
|
||||
// |__| \/ \/
|
||||
//
|
||||
|
||||
// UploadRepoFileForm form for uploading repository file
|
||||
type UploadRepoFileForm struct {
|
||||
TreePath string `binding:"MaxSize(500)"`
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
Files []string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// RemoveUploadFileForm form for removing uploaded file
|
||||
type RemoveUploadFileForm struct {
|
||||
File string `binding:"Required;MaxSize(50)"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ________ .__ __
|
||||
// \______ \ ____ | | _____/ |_ ____
|
||||
// | | \_/ __ \| | _/ __ \ __\/ __ \
|
||||
// | ` \ ___/| |_\ ___/| | \ ___/
|
||||
// /_______ /\___ >____/\___ >__| \___ >
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// DeleteRepoFileForm form for deleting repository file
|
||||
type DeleteRepoFileForm struct {
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ___________.__ ___________ __
|
||||
// \__ ___/|__| _____ ____ \__ ___/___________ ____ | | __ ___________
|
||||
// | | | |/ \_/ __ \ | | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
|
||||
|
57
services/forms/repo_form_editor.go
Normal file
57
services/forms/repo_form_editor.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
)
|
||||
|
||||
type CommitCommonForm struct {
|
||||
TreePath string `binding:"MaxSize(500)"`
|
||||
CommitSummary string `binding:"MaxSize(100)"`
|
||||
CommitMessage string
|
||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||
LastCommit string
|
||||
Signoff bool
|
||||
CommitEmail string
|
||||
}
|
||||
|
||||
func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
type CommitCommonFormInterface interface {
|
||||
GetCommitCommonForm() *CommitCommonForm
|
||||
}
|
||||
|
||||
func (f *CommitCommonForm) GetCommitCommonForm() *CommitCommonForm {
|
||||
return f
|
||||
}
|
||||
|
||||
type EditRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
Content optional.Option[string]
|
||||
}
|
||||
|
||||
type DeleteRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
}
|
||||
|
||||
type UploadRepoFileForm struct {
|
||||
CommitCommonForm
|
||||
Files []string
|
||||
}
|
||||
|
||||
type CherryPickForm struct {
|
||||
CommitCommonForm
|
||||
Revert bool
|
||||
}
|
@ -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,
|
||||
|
@ -134,9 +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 {
|
||||
// Rebase the filename
|
||||
// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
|
||||
func CleanGitTreePath(name string) string {
|
||||
name = util.PathJoinRel(name)
|
||||
// Git disallows any filenames to have a .git directory in them.
|
||||
for part := range strings.SplitSeq(name, "/") {
|
||||
@ -144,5 +143,8 @@ func CleanUploadFileName(name string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if name == "." {
|
||||
name = ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -88,8 +88,26 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
type LazyReadSeeker interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
OpenLazyReader() error
|
||||
}
|
||||
|
||||
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
|
||||
func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) {
|
||||
func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) {
|
||||
var addedLfsPointers []lfs.Pointer
|
||||
defer func() {
|
||||
if errRet != nil {
|
||||
for _, lfsPointer := range addedLfsPointers {
|
||||
_, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid)
|
||||
if err != nil {
|
||||
log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := repo.MustNotBeArchived()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -127,14 +145,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,
|
||||
@ -241,10 +259,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||
lfsContentStore := lfs.NewContentStore()
|
||||
for _, file := range opts.Files {
|
||||
switch file.Operation {
|
||||
case "create", "update", "rename":
|
||||
if err = CreateUpdateRenameFile(ctx, t, file, lfsContentStore, repo.ID, hasOldBranch); err != nil {
|
||||
case "create", "update", "rename", "upload":
|
||||
addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addedLfsPointer != nil {
|
||||
addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
|
||||
}
|
||||
case "delete":
|
||||
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
||||
return nil, err
|
||||
@ -366,18 +388,29 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
||||
|
||||
// handles the check for various issues for ChangeRepoFiles
|
||||
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
|
||||
if file.Operation == "update" || file.Operation == "delete" || file.Operation == "rename" {
|
||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
||||
if err != nil {
|
||||
return err
|
||||
// check old entry (fromTreePath/fromEntry)
|
||||
if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" {
|
||||
var fromEntryIDString string
|
||||
{
|
||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
||||
if file.Operation == "upload" && git.IsErrNotExist(err) {
|
||||
fromEntry = nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if fromEntry != nil {
|
||||
fromEntryIDString = fromEntry.ID.String()
|
||||
file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
|
||||
}
|
||||
}
|
||||
|
||||
if file.SHA != "" {
|
||||
// If the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||
if file.SHA != fromEntry.ID.String() {
|
||||
if file.SHA != fromEntryIDString {
|
||||
return pull_service.ErrSHADoesNotMatch{
|
||||
Path: file.Options.treePath,
|
||||
GivenSHA: file.SHA,
|
||||
CurrentSHA: fromEntry.ID.String(),
|
||||
CurrentSHA: fromEntryIDString,
|
||||
}
|
||||
}
|
||||
} else if opts.LastCommitID != "" {
|
||||
@ -399,11 +432,10 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
|
||||
// haven't been made. We throw an error if one wasn't provided.
|
||||
return ErrSHAOrCommitIDNotProvided{}
|
||||
}
|
||||
// FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
|
||||
file.Options.executable = fromEntry.IsExecutable()
|
||||
}
|
||||
|
||||
if file.Operation == "create" || file.Operation == "update" || file.Operation == "rename" {
|
||||
// check new entry (treePath/treeEntry)
|
||||
if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" {
|
||||
// For operation's target path, we need to make sure no parts of the path are existing files or links
|
||||
// except for the last item in the path (which is the file name).
|
||||
// And that shouldn't exist IF it is a new file OR is being moved to a new path.
|
||||
@ -454,18 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
|
||||
func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) {
|
||||
if rd, ok := file.ContentReader.(LazyReadSeeker); ok {
|
||||
if err := rd.OpenLazyReader(); err != nil {
|
||||
return nil, fmt.Errorf("OpenLazyReader: %w", err)
|
||||
}
|
||||
defer rd.Close()
|
||||
}
|
||||
|
||||
// Get the two paths (might be the same if not moving) from the index if they exist
|
||||
filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRepoFile: %w", err)
|
||||
return nil, fmt.Errorf("LsFiles: %w", err)
|
||||
}
|
||||
// If is a new file (not updating) then the given path shouldn't exist
|
||||
if file.Operation == "create" {
|
||||
if slices.Contains(filesInIndex, file.TreePath) {
|
||||
return ErrRepoFileAlreadyExists{
|
||||
Path: file.TreePath,
|
||||
}
|
||||
return nil, ErrRepoFileAlreadyExists{Path: file.TreePath}
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,7 +511,7 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
|
||||
for _, indexFile := range filesInIndex {
|
||||
if indexFile == file.Options.fromTreePath {
|
||||
if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -482,45 +519,46 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
|
||||
|
||||
var writeObjectRet *writeRepoObjectRet
|
||||
switch file.Operation {
|
||||
case "create", "update":
|
||||
writeObjectRet, err = writeRepoObjectForCreateOrUpdate(ctx, t, file)
|
||||
case "create", "update", "upload":
|
||||
writeObjectRet, err = writeRepoObjectForModify(ctx, t, file)
|
||||
case "rename":
|
||||
writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
|
||||
default:
|
||||
return util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
|
||||
return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach)
|
||||
if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if writeObjectRet.LfsContent == nil {
|
||||
return nil // No LFS pointer, so nothing to do
|
||||
return nil, nil // No LFS pointer, so nothing to do
|
||||
}
|
||||
defer writeObjectRet.LfsContent.Close()
|
||||
|
||||
// Now we must store the content into an LFS object
|
||||
lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if exist, err := contentStore.Exists(lfsMetaObject.Pointer); err != nil {
|
||||
return err
|
||||
} else if exist {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
|
||||
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
|
||||
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
|
||||
if err != nil {
|
||||
if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
|
||||
return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return err
|
||||
return &lfsMetaObject.Pointer, nil
|
||||
}
|
||||
|
||||
func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) {
|
||||
@ -544,8 +582,8 @@ type writeRepoObjectRet struct {
|
||||
LfsPointer lfs.Pointer
|
||||
}
|
||||
|
||||
// writeRepoObjectForCreateOrUpdate hashes the git object for create or update operations
|
||||
func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
// writeRepoObjectForModify hashes the git object for create or update operations
|
||||
func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
ret = &writeRepoObjectRet{}
|
||||
treeObjectContentReader := file.ContentReader
|
||||
if setting.LFS.StartServer {
|
||||
@ -574,7 +612,7 @@ func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRep
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// writeRepoObjectForRename the same as writeRepoObjectForCreateOrUpdate buf for "rename"
|
||||
// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename"
|
||||
func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||
lastCommitID, err := t.GetLastCommit(ctx)
|
||||
if err != nil {
|
||||
|
@ -8,15 +8,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/attribute"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// UploadRepoFileOptions contains the uploaded repository file options
|
||||
@ -32,23 +28,48 @@ type UploadRepoFileOptions struct {
|
||||
Committer *IdentityOptions
|
||||
}
|
||||
|
||||
type uploadInfo struct {
|
||||
upload *repo_model.Upload
|
||||
lfsMetaObject *git_model.LFSMetaObject
|
||||
type lazyLocalFileReader struct {
|
||||
*os.File
|
||||
localFilename string
|
||||
counter int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
|
||||
for _, info := range *infos {
|
||||
if info.lfsMetaObject == nil {
|
||||
continue
|
||||
}
|
||||
if !info.lfsMetaObject.Existing {
|
||||
if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
|
||||
original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
|
||||
var _ LazyReadSeeker = (*lazyLocalFileReader)(nil)
|
||||
|
||||
func (l *lazyLocalFileReader) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.counter > 0 {
|
||||
l.counter--
|
||||
if l.counter == 0 {
|
||||
if err := l.File.Close(); err != nil {
|
||||
return fmt.Errorf("close file %s: %w", l.localFilename, err)
|
||||
}
|
||||
l.File = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return original
|
||||
return fmt.Errorf("file %s already closed", l.localFilename)
|
||||
}
|
||||
|
||||
func (l *lazyLocalFileReader) OpenLazyReader() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.File != nil {
|
||||
l.counter++
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(l.localFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.File = file
|
||||
l.counter = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadRepoFiles uploads files to the given repository
|
||||
@ -62,178 +83,29 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||
return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
|
||||
}
|
||||
|
||||
names := make([]string, len(uploads))
|
||||
infos := make([]uploadInfo, len(uploads))
|
||||
for i, upload := range uploads {
|
||||
// Check file is not lfs locked, will return nil if lock setting not enabled
|
||||
filepath := path.Join(opts.TreePath, upload.Name)
|
||||
lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lfsLock != nil && lfsLock.OwnerID != doer.ID {
|
||||
u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
|
||||
}
|
||||
|
||||
names[i] = upload.Name
|
||||
infos[i] = uploadInfo{upload: upload}
|
||||
changeOpts := &ChangeRepoFilesOptions{
|
||||
LastCommitID: opts.LastCommitID,
|
||||
OldBranch: opts.OldBranch,
|
||||
NewBranch: opts.NewBranch,
|
||||
Message: opts.Message,
|
||||
Signoff: opts.Signoff,
|
||||
Author: opts.Author,
|
||||
Committer: opts.Committer,
|
||||
}
|
||||
|
||||
t, err := NewTemporaryUploadRepository(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
hasOldBranch := true
|
||||
if err = t.Clone(ctx, opts.OldBranch, true); err != nil {
|
||||
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||
return err
|
||||
}
|
||||
if err = t.Init(ctx, repo.ObjectFormatName); err != nil {
|
||||
return err
|
||||
}
|
||||
hasOldBranch = false
|
||||
opts.LastCommitID = ""
|
||||
}
|
||||
if hasOldBranch {
|
||||
if err = t.SetDefaultIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var attributesMap map[string]*attribute.Attributes
|
||||
// when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist
|
||||
if setting.LFS.StartServer {
|
||||
attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
|
||||
Attributes: []string{attribute.Filter},
|
||||
Filenames: names,
|
||||
for _, upload := range uploads {
|
||||
changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{
|
||||
Operation: "upload",
|
||||
TreePath: path.Join(opts.TreePath, upload.Name),
|
||||
ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Copy uploaded files into repository.
|
||||
// TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop,
|
||||
// so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future:
|
||||
// 1. Add ".gitattributes" to git index
|
||||
// 2. Run "check-attr" (the previous attribute.CheckAttributes call)
|
||||
// 3. Add files to git index (this loop)
|
||||
// This problem is trivial so maybe no need to spend too much time on it at the moment.
|
||||
for i := range infos {
|
||||
if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Now write the tree
|
||||
treeHash, err := t.WriteTree(ctx)
|
||||
_, err = ChangeRepoFiles(ctx, repo, doer, changeOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now commit the tree
|
||||
commitOpts := &CommitTreeUserOptions{
|
||||
ParentCommitID: opts.LastCommitID,
|
||||
TreeHash: treeHash,
|
||||
CommitMessage: opts.Message,
|
||||
SignOff: opts.Signoff,
|
||||
DoerUser: doer,
|
||||
AuthorIdentity: opts.Author,
|
||||
CommitterIdentity: opts.Committer,
|
||||
}
|
||||
commitHash, err := t.CommitTree(ctx, commitOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now deal with LFS objects
|
||||
for i := range infos {
|
||||
if infos[i].lfsMetaObject == nil {
|
||||
continue
|
||||
}
|
||||
infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
// OK Now we need to cleanup
|
||||
return cleanUpAfterFailure(ctx, &infos, t, err)
|
||||
}
|
||||
// Don't move the files yet - we need to ensure that
|
||||
// everything can be inserted first
|
||||
}
|
||||
|
||||
// OK now we can insert the data into the store - there's no way to clean up the store
|
||||
// once it's in there, it's in there.
|
||||
contentStore := lfs.NewContentStore()
|
||||
for _, info := range infos {
|
||||
if err := uploadToLFSContentStore(info, contentStore); err != nil {
|
||||
return cleanUpAfterFailure(ctx, &infos, t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Then push this tree to NewBranch
|
||||
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo_model.DeleteUploads(ctx, uploads...)
|
||||
}
|
||||
|
||||
func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error {
|
||||
file, err := os.Open(info.upload.LocalPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var objectHash string
|
||||
if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" {
|
||||
// Handle LFS
|
||||
// FIXME: Inefficient! this should probably happen in models.Upload
|
||||
pointer, err := lfs.GeneratePointer(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
|
||||
|
||||
if objectHash, err = t.HashObjectAndWrite(ctx, strings.NewReader(pointer.StringContent())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if objectHash, err = t.HashObjectAndWrite(ctx, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the object to the index
|
||||
return t.AddObjectToIndex(ctx, "100644", objectHash, path.Join(treePath, info.upload.Name))
|
||||
}
|
||||
|
||||
func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error {
|
||||
if info.lfsMetaObject == nil {
|
||||
return nil
|
||||
}
|
||||
exist, err := contentStore.Exists(info.lfsMetaObject.Pointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
file, err := os.Open(info.upload.LocalPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
// FIXME: Put regenerates the hash and copies the file over.
|
||||
// I guess this strictly ensures the soundness of the store but this is inefficient.
|
||||
if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil {
|
||||
// OK Now we need to cleanup
|
||||
// Can't clean up the store, once uploaded there they're there.
|
||||
return err
|
||||
}
|
||||
if err := repo_model.DeleteUploads(ctx, uploads...); err != nil {
|
||||
log.Error("DeleteUploads: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -3,15 +3,13 @@
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}">
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="true">
|
||||
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
|
||||
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
|
||||
<div class="breadcrumb">
|
||||
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}}
|
||||
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}}
|
||||
{{if eq .CherryPickType "revert"}}
|
||||
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
|
||||
{{else}}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<div class="commit-form-wrapper">
|
||||
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
||||
<div class="commit-form">
|
||||
<h3>{{- if .CanCommitToBranch.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
<h3>{{- if .CommitFormBehaviors.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
||||
{{- else}}
|
||||
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
|
||||
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
|
||||
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
|
||||
{{- end}}</h3>
|
||||
<div class="field">
|
||||
@ -22,17 +22,17 @@
|
||||
</div>
|
||||
<div class="quick-pull-choice js-quick-pull-choice">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}">
|
||||
<div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
|
||||
<label>
|
||||
{{svg "octicon-git-commit"}}
|
||||
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
|
||||
{{if not .CanCommitToBranch.CanCommitToBranch}}
|
||||
{{if not .CommitFormBehaviors.CanCommitToBranch}}
|
||||
<div class="ui visible small warning message">
|
||||
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
||||
<ul>
|
||||
{{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||
{{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
@ -42,14 +42,14 @@
|
||||
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
{{if .CanCreatePullRequest}}
|
||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
|
||||
{{else}}
|
||||
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
|
||||
{{end}}
|
||||
<label>
|
||||
{{svg "octicon-git-pull-request"}}
|
||||
{{if .CanCreatePullRequest}}
|
||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
||||
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
|
||||
<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
|
||||
<div class="new-branch-name-input field">
|
||||
{{svg "octicon-git-branch"}}
|
||||
<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
|
||||
<span class="text-muted js-quick-pull-normalization-info"></span>
|
||||
@ -67,7 +67,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
|
||||
<div class="field {{if .Err_CommitEmail}}error{{end}}">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
|
||||
<select class="ui selection dropdown" name="commit_email">
|
||||
{{- range $email := .CommitCandidateEmails -}}
|
||||
@ -77,7 +77,8 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}>
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<button id="commit-button" type="submit" class="ui primary button">
|
||||
{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
|
||||
</button>
|
||||
<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
|
||||
|
16
templates/repo/editor/common_breadcrumb.tmpl
Normal file
16
templates/repo/editor/common_breadcrumb.tmpl
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="breadcrumb">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{or .ReturnURI (print $.BranchLink "/" (PathEscapeSegments .TreePath))}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}">
|
||||
</div>
|
@ -3,9 +3,8 @@
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui form" method="post">
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -3,30 +3,13 @@
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post"
|
||||
<form class="ui edit form form-fetch-action" method="post"
|
||||
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
|
||||
</div>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
{{if not .NotEditableReason}}
|
||||
<div class="field">
|
||||
|
@ -3,15 +3,13 @@
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
|
||||
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
|
||||
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
<div class="breadcrumb">
|
||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
|
||||
<div class="breadcrumb-divider">:</div>
|
||||
|
@ -3,25 +3,10 @@
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<form class="ui comment form" method="post">
|
||||
<form class="ui comment form form-fetch-action" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
||||
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
|
||||
{{$n := len .TreeNames}}
|
||||
{{$l := Eval $n "-" 1}}
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{.TreePath | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span>
|
||||
<input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required>
|
||||
</div>
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{template "repo/upload" .}}
|
||||
|
@ -69,7 +69,7 @@
|
||||
|
||||
{{if not $isTreePathRoot}}
|
||||
{{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}}
|
||||
<span class="breadcrumb repo-path tw-ml-1">
|
||||
<span class="breadcrumb">
|
||||
<a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
|
||||
{{- range $i, $v := .TreeNames -}}
|
||||
<span class="breadcrumb-divider">/</span>
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) {
|
||||
"content": "package main",
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
|
||||
// let gitea calculate language stats
|
||||
time.Sleep(time.Second)
|
||||
|
@ -12,12 +12,13 @@ import (
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testLicenseContent = `
|
||||
Copyright (c) 2024 Gitea
|
||||
Copyright (c) 2024 Gitea
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@ -48,7 +49,8 @@ func TestAPIRepoLicense(t *testing.T) {
|
||||
"content": testLicenseContent,
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
|
||||
// let gitea update repo license
|
||||
time.Sleep(time.Second)
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@ -34,7 +35,7 @@ func TestCreateFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder {
|
||||
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
|
||||
// Request editor page
|
||||
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
|
||||
req := NewRequest(t, "GET", newURL)
|
||||
@ -52,7 +53,8 @@ func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, file
|
||||
"content": content,
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
return session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
}
|
||||
|
||||
func TestCreateFileOnProtectedBranch(t *testing.T) {
|
||||
@ -88,9 +90,9 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
// Check body for error message
|
||||
assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".")
|
||||
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
respErr := test.ParseJSONError(resp.Body.Bytes())
|
||||
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
|
||||
|
||||
// remove the protected branch
|
||||
csrf = GetUserCSRFToken(t, session)
|
||||
@ -131,7 +133,8 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
|
||||
"commit_choice": "direct",
|
||||
},
|
||||
)
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
|
||||
// Verify the change
|
||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
|
||||
@ -161,7 +164,8 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
|
||||
"new_branch_name": targetBranch,
|
||||
},
|
||||
)
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
|
||||
// Verify the change
|
||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
|
||||
@ -211,9 +215,8 @@ func TestWebGitCommitEmail(t *testing.T) {
|
||||
newCommit := getLastCommit(t)
|
||||
if expectedUserName == "" {
|
||||
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
errMsg := htmlDoc.doc.Find(".ui.negative.message").Text()
|
||||
assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"))
|
||||
respErr := test.ParseJSONError(resp.Body.Bytes())
|
||||
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
|
||||
} else {
|
||||
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||
assert.Equal(t, expectedUserName, newCommit.Author.Name)
|
||||
@ -333,7 +336,7 @@ index 0000000000..bbbbbbbbbb
|
||||
)
|
||||
|
||||
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch
|
||||
assert.Equal(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location"))
|
||||
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -30,7 +29,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
|
||||
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) {
|
||||
url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
@ -38,7 +37,8 @@ func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, tree
|
||||
"tree_path": treePath,
|
||||
"content": content,
|
||||
})
|
||||
return session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
}
|
||||
|
||||
func TestEmptyRepo(t *testing.T) {
|
||||
@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) {
|
||||
"content": "newly-added-test-file",
|
||||
})
|
||||
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
redirect := test.RedirectURL(resp)
|
||||
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect)
|
||||
|
||||
@ -154,9 +154,9 @@ func TestEmptyRepoUploadFile(t *testing.T) {
|
||||
"files": respMap["uuid"],
|
||||
"tree_path": "",
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
redirect := test.RedirectURL(resp)
|
||||
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect)
|
||||
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch, redirect)
|
||||
|
||||
req = NewRequest(t, "GET", redirect)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
@ -159,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) {
|
||||
"commit_summary": "user2 updated the file",
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
user2Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,14 +1,10 @@
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.breadcrumb .breadcrumb-divider {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.breadcrumb > * {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -139,11 +139,6 @@ td .commit-summary {
|
||||
}
|
||||
}
|
||||
|
||||
.repo-path {
|
||||
display: flex;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.repository.file.list .non-diff-file-content .header .icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
|
||||
}
|
||||
|
||||
const formMethod = formEl.getAttribute('method') || 'get';
|
||||
const formActionUrl = formEl.getAttribute('action');
|
||||
const formActionUrl = formEl.getAttribute('action') || window.location.href;
|
||||
const formData = new FormData(formEl);
|
||||
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
|
||||
if (submitterName) {
|
||||
|
@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {submitFormFetchAction} from './common-fetch-action.ts';
|
||||
|
||||
function initEditPreviewTab(elForm: HTMLFormElement) {
|
||||
const elTabMenu = elForm.querySelector('.repo-editor-menu');
|
||||
@ -143,31 +144,28 @@ export function initRepoEditor() {
|
||||
|
||||
const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
|
||||
|
||||
// on the upload page, there is no editor(textarea)
|
||||
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
|
||||
if (!editArea) return;
|
||||
|
||||
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
|
||||
// to enable or disable the commit button
|
||||
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
|
||||
const dirtyFileClass = 'dirty-file';
|
||||
|
||||
// Enabling the button at the start if the page has posted
|
||||
if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') {
|
||||
commitButton.disabled = false;
|
||||
}
|
||||
|
||||
const syncCommitButtonState = () => {
|
||||
const dirty = elForm.classList.contains(dirtyFileClass);
|
||||
commitButton.disabled = !dirty;
|
||||
};
|
||||
// Registering a custom listener for the file path and the file content
|
||||
// FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
|
||||
applyAreYouSure(elForm, {
|
||||
silent: true,
|
||||
dirtyClass: dirtyFileClass,
|
||||
fieldSelector: ':input:not(.commit-form-wrapper :input)',
|
||||
change($form: any) {
|
||||
const dirty = $form[0]?.classList.contains(dirtyFileClass);
|
||||
commitButton.disabled = !dirty;
|
||||
},
|
||||
change: syncCommitButtonState,
|
||||
});
|
||||
|
||||
// on the upload page, there is no editor(textarea)
|
||||
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
|
||||
if (!editArea) return;
|
||||
syncCommitButtonState(); // disable the "commit" button when no content changes
|
||||
|
||||
initEditPreviewTab(elForm);
|
||||
|
||||
@ -182,7 +180,7 @@ export function initRepoEditor() {
|
||||
editor.setValue(value);
|
||||
}
|
||||
|
||||
commitButton?.addEventListener('click', async (e) => {
|
||||
commitButton.addEventListener('click', async (e) => {
|
||||
// A modal which asks if an empty file should be committed
|
||||
if (!editArea.value) {
|
||||
e.preventDefault();
|
||||
@ -191,7 +189,7 @@ export function initRepoEditor() {
|
||||
content: elForm.getAttribute('data-text-empty-confirm-content'),
|
||||
})) {
|
||||
ignoreAreYouSure(elForm);
|
||||
elForm.submit();
|
||||
submitFormFetchAction(elForm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user