mirror of
https://github.com/go-gitea/gitea.git
synced 2025-06-22 06:17:53 +00:00
parent
81adb01713
commit
4fc626daa1
@ -112,7 +112,6 @@ type LFSMetaObject struct {
|
|||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
lfs.Pointer `xorm:"extends"`
|
lfs.Pointer `xorm:"extends"`
|
||||||
RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||||
Existing bool `xorm:"-"`
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
}
|
}
|
||||||
@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if exist {
|
} else if exist {
|
||||||
m.Existing = true
|
|
||||||
return m, committer.Commit()
|
return m, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ func (o *UpdateFileOptions) Branch() string {
|
|||||||
return o.FileOptions.BranchName
|
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
|
// ChangeFileOperation for creating, updating or deleting a file
|
||||||
type ChangeFileOperation struct {
|
type ChangeFileOperation struct {
|
||||||
// indicates what to do with the file
|
// indicates what to do with the file
|
||||||
|
@ -1354,7 +1354,7 @@ editor.update = Update %s
|
|||||||
editor.delete = Delete %s
|
editor.delete = Delete %s
|
||||||
editor.patch = Apply Patch
|
editor.patch = Apply Patch
|
||||||
editor.patching = Patching:
|
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.new_patch = New Patch
|
||||||
editor.commit_message_desc = Add an optional extended description…
|
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.
|
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.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.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.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_modifying_no_longer_exists = The file being modified, "%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_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_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.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.
|
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_header = Commit an empty file
|
||||||
editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
|
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.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_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 = The change was rejected by the server. Please check Git Hooks.
|
||||||
editor.push_rejected_summary = Full Rejection Message:
|
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.require_signed_commit = Branch requires a signed commit
|
||||||
editor.cherry_pick = Cherry-pick %s onto:
|
editor.cherry_pick = Cherry-pick %s onto:
|
||||||
editor.revert = Revert %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.desc = Browse source code change history.
|
||||||
commits.commits = Commits
|
commits.commits = Commits
|
||||||
|
@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||||
return
|
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{
|
changeRepoFile := &files_service.ChangeRepoFile{
|
||||||
Operation: file.Operation,
|
Operation: file.Operation,
|
||||||
TreePath: file.Path,
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCleanUploadName(t *testing.T) {
|
func TestEditorUtils(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
kases := map[string]string{
|
t.Run("getUniquePatchBranchName", func(t *testing.T) {
|
||||||
".git/refs/master": "",
|
branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
|
||||||
"/root/abc": "root/abc",
|
assert.Equal(t, "user2-patch-1", branchName)
|
||||||
"./../../abc": "abc",
|
})
|
||||||
"a/../.git": "",
|
t.Run("getClosestParentWithFiles", func(t *testing.T) {
|
||||||
"a/../../../abc": "abc",
|
gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||||
"../../../acd": "acd",
|
defer gitRepo.Close()
|
||||||
"../../.git/abc": "",
|
treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
|
||||||
"..\\..\\.git/abc": "..\\..\\.git/abc",
|
assert.Equal(t, "docs", treePath)
|
||||||
"..\\../.git/abc": "",
|
treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
|
||||||
"..\\../.git": "",
|
assert.Empty(t, treePath)
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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("/{username}/{reponame}", func() { // repo code
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
|
m.Post("/_preview/*", repo.DiffPreviewPost)
|
||||||
m.Combo("/_edit/*").Get(repo.EditFile).
|
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)
|
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).
|
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
||||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
||||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
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)
|
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/upload-file", repo.UploadFileToServer)
|
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.MustBeAbleToUpload, reqRepoCodeWriter)
|
||||||
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
|
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
|
||||||
|
|
||||||
|
@ -94,24 +94,22 @@ func RepoMustNotBeArchived() func(ctx *Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCommitToBranchResults represents the results of CanCommitToBranch
|
type CommitFormBehaviors struct {
|
||||||
type CanCommitToBranchResults struct {
|
CanCommitToBranch bool
|
||||||
CanCommitToBranch bool
|
EditorEnabled bool
|
||||||
EditorEnabled bool
|
UserCanPush bool
|
||||||
UserCanPush bool
|
RequireSigned bool
|
||||||
RequireSigned bool
|
WillSign bool
|
||||||
WillSign bool
|
SigningKey *git.SigningKey
|
||||||
SigningKey *git.SigningKey
|
WontSignReason string
|
||||||
WontSignReason string
|
CanCreatePullRequest bool
|
||||||
|
CanCreateBasePullRequest bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanCommitToBranch returns true if repository is editable and user has proper access level
|
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
|
||||||
//
|
|
||||||
// and branch is not protected for push
|
|
||||||
func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
|
|
||||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CanCommitToBranchResults{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
userCanPush := true
|
userCanPush := true
|
||||||
requireSigned := false
|
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,
|
CanCommitToBranch: canCommit,
|
||||||
EditorEnabled: canEnableEditor,
|
EditorEnabled: canEnableEditor,
|
||||||
UserCanPush: userCanPush,
|
UserCanPush: userCanPush,
|
||||||
@ -146,6 +147,9 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
|
|||||||
WillSign: sign,
|
WillSign: sign,
|
||||||
SigningKey: keyID,
|
SigningKey: keyID,
|
||||||
WontSignReason: wontSignReason,
|
WontSignReason: wontSignReason,
|
||||||
|
|
||||||
|
CanCreatePullRequest: canCreatePullRequest,
|
||||||
|
CanCreateBasePullRequest: canCreateBasePullRequest,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,5 +113,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
|
|||||||
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
|
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
|
||||||
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
|
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
|
||||||
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
|
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"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
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/structs"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
"code.gitea.io/gitea/services/context"
|
"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)
|
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)
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
cleanTreePath := CleanUploadFileName(treePath)
|
cleanTreePath := CleanGitTreePath(treePath)
|
||||||
if cleanTreePath == "" && treePath != "" {
|
if cleanTreePath == "" && treePath != "" {
|
||||||
return nil, ErrFilenameInvalid{
|
return nil, ErrFilenameInvalid{
|
||||||
Path: treePath,
|
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
|
// 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) {
|
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)
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
cleanTreePath := CleanUploadFileName(treePath)
|
cleanTreePath := CleanGitTreePath(treePath)
|
||||||
if cleanTreePath == "" && treePath != "" {
|
if cleanTreePath == "" && treePath != "" {
|
||||||
return nil, ErrFilenameInvalid{
|
return nil, ErrFilenameInvalid{
|
||||||
Path: treePath,
|
Path: treePath,
|
||||||
|
@ -134,9 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error {
|
|||||||
return util.ErrInvalidArgument
|
return util.ErrInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
|
// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
|
||||||
func CleanUploadFileName(name string) string {
|
func CleanGitTreePath(name string) string {
|
||||||
// Rebase the filename
|
|
||||||
name = util.PathJoinRel(name)
|
name = util.PathJoinRel(name)
|
||||||
// Git disallows any filenames to have a .git directory in them.
|
// Git disallows any filenames to have a .git directory in them.
|
||||||
for part := range strings.SplitSeq(name, "/") {
|
for part := range strings.SplitSeq(name, "/") {
|
||||||
@ -144,5 +143,8 @@ func CleanUploadFileName(name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if name == "." {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
@ -10,17 +10,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCleanUploadFileName(t *testing.T) {
|
func TestCleanUploadFileName(t *testing.T) {
|
||||||
t.Run("Clean regular file", func(t *testing.T) {
|
assert.Equal(t, "", CleanGitTreePath("")) //nolint
|
||||||
name := "this/is/test"
|
assert.Equal(t, "", CleanGitTreePath(".")) //nolint
|
||||||
cleanName := CleanUploadFileName(name)
|
assert.Equal(t, "a/b", CleanGitTreePath("a/b"))
|
||||||
expectedCleanName := name
|
assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint
|
||||||
assert.Equal(t, expectedCleanName, cleanName)
|
assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Clean a .git path", func(t *testing.T) {
|
|
||||||
name := "this/is/test/.git"
|
|
||||||
cleanName := CleanUploadFileName(name)
|
|
||||||
expectedCleanName := ""
|
|
||||||
assert.Equal(t, expectedCleanName, cleanName)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -88,8 +88,26 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error {
|
|||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LazyReadSeeker interface {
|
||||||
|
io.ReadSeeker
|
||||||
|
io.Closer
|
||||||
|
OpenLazyReader() error
|
||||||
|
}
|
||||||
|
|
||||||
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
|
// 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()
|
err := repo.MustNotBeArchived()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
treePath := CleanUploadFileName(file.TreePath)
|
treePath := CleanGitTreePath(file.TreePath)
|
||||||
if treePath == "" {
|
if treePath == "" {
|
||||||
return nil, ErrFilenameInvalid{
|
return nil, ErrFilenameInvalid{
|
||||||
Path: file.TreePath,
|
Path: file.TreePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If there is a fromTreePath (we are copying it), also clean it up
|
// 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 != "" {
|
if fromTreePath == "" && file.FromTreePath != "" {
|
||||||
return nil, ErrFilenameInvalid{
|
return nil, ErrFilenameInvalid{
|
||||||
Path: file.FromTreePath,
|
Path: file.FromTreePath,
|
||||||
@ -241,10 +259,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||||||
lfsContentStore := lfs.NewContentStore()
|
lfsContentStore := lfs.NewContentStore()
|
||||||
for _, file := range opts.Files {
|
for _, file := range opts.Files {
|
||||||
switch file.Operation {
|
switch file.Operation {
|
||||||
case "create", "update", "rename":
|
case "create", "update", "rename", "upload":
|
||||||
if err = CreateUpdateRenameFile(ctx, t, file, lfsContentStore, repo.ID, hasOldBranch); err != nil {
|
addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if addedLfsPointer != nil {
|
||||||
|
addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
|
||||||
|
}
|
||||||
case "delete":
|
case "delete":
|
||||||
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -366,18 +388,29 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
|||||||
|
|
||||||
// handles the check for various issues for ChangeRepoFiles
|
// handles the check for various issues for ChangeRepoFiles
|
||||||
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
|
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
|
||||||
if file.Operation == "update" || file.Operation == "delete" || file.Operation == "rename" {
|
// check old entry (fromTreePath/fromEntry)
|
||||||
fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
|
if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" {
|
||||||
if err != nil {
|
var fromEntryIDString string
|
||||||
return err
|
{
|
||||||
|
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 file.SHA != "" {
|
||||||
// If the SHA given doesn't match the SHA of the fromTreePath, throw error
|
// 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{
|
return pull_service.ErrSHADoesNotMatch{
|
||||||
Path: file.Options.treePath,
|
Path: file.Options.treePath,
|
||||||
GivenSHA: file.SHA,
|
GivenSHA: file.SHA,
|
||||||
CurrentSHA: fromEntry.ID.String(),
|
CurrentSHA: fromEntryIDString,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if opts.LastCommitID != "" {
|
} 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.
|
// haven't been made. We throw an error if one wasn't provided.
|
||||||
return ErrSHAOrCommitIDNotProvided{}
|
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
|
// 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).
|
// 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.
|
// 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
|
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
|
// 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)
|
filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath)
|
||||||
if err != nil {
|
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 is a new file (not updating) then the given path shouldn't exist
|
||||||
if file.Operation == "create" {
|
if file.Operation == "create" {
|
||||||
if slices.Contains(filesInIndex, file.TreePath) {
|
if slices.Contains(filesInIndex, file.TreePath) {
|
||||||
return ErrRepoFileAlreadyExists{
|
return nil, ErrRepoFileAlreadyExists{Path: file.TreePath}
|
||||||
Path: file.TreePath,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,7 +511,7 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f
|
|||||||
for _, indexFile := range filesInIndex {
|
for _, indexFile := range filesInIndex {
|
||||||
if indexFile == file.Options.fromTreePath {
|
if indexFile == file.Options.fromTreePath {
|
||||||
if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil {
|
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
|
var writeObjectRet *writeRepoObjectRet
|
||||||
switch file.Operation {
|
switch file.Operation {
|
||||||
case "create", "update":
|
case "create", "update", "upload":
|
||||||
writeObjectRet, err = writeRepoObjectForCreateOrUpdate(ctx, t, file)
|
writeObjectRet, err = writeRepoObjectForModify(ctx, t, file)
|
||||||
case "rename":
|
case "rename":
|
||||||
writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
|
writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
|
||||||
default:
|
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 {
|
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)
|
// 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 {
|
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 {
|
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()
|
defer writeObjectRet.LfsContent.Close()
|
||||||
|
|
||||||
// Now we must store the content into an LFS object
|
// Now we must store the content into an LFS object
|
||||||
lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
|
lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if exist, err := contentStore.Exists(lfsMetaObject.Pointer); err != nil {
|
exist, err := contentStore.Exists(lfsMetaObject.Pointer)
|
||||||
return err
|
|
||||||
} else if exist {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
|
return nil, err
|
||||||
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, 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) {
|
func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) {
|
||||||
@ -544,8 +582,8 @@ type writeRepoObjectRet struct {
|
|||||||
LfsPointer lfs.Pointer
|
LfsPointer lfs.Pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeRepoObjectForCreateOrUpdate hashes the git object for create or update operations
|
// writeRepoObjectForModify hashes the git object for create or update operations
|
||||||
func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||||
ret = &writeRepoObjectRet{}
|
ret = &writeRepoObjectRet{}
|
||||||
treeObjectContentReader := file.ContentReader
|
treeObjectContentReader := file.ContentReader
|
||||||
if setting.LFS.StartServer {
|
if setting.LFS.StartServer {
|
||||||
@ -574,7 +612,7 @@ func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRep
|
|||||||
return ret, nil
|
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) {
|
func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
|
||||||
lastCommitID, err := t.GetLastCommit(ctx)
|
lastCommitID, err := t.GetLastCommit(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,15 +8,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"sync"
|
||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/git/attribute"
|
|
||||||
"code.gitea.io/gitea/modules/lfs"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadRepoFileOptions contains the uploaded repository file options
|
// UploadRepoFileOptions contains the uploaded repository file options
|
||||||
@ -32,23 +28,48 @@ type UploadRepoFileOptions struct {
|
|||||||
Committer *IdentityOptions
|
Committer *IdentityOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type uploadInfo struct {
|
type lazyLocalFileReader struct {
|
||||||
upload *repo_model.Upload
|
*os.File
|
||||||
lfsMetaObject *git_model.LFSMetaObject
|
localFilename string
|
||||||
|
counter int
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
|
var _ LazyReadSeeker = (*lazyLocalFileReader)(nil)
|
||||||
for _, info := range *infos {
|
|
||||||
if info.lfsMetaObject == nil {
|
func (l *lazyLocalFileReader) Close() error {
|
||||||
continue
|
l.mu.Lock()
|
||||||
}
|
defer l.mu.Unlock()
|
||||||
if !info.lfsMetaObject.Existing {
|
|
||||||
if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
|
if l.counter > 0 {
|
||||||
original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
|
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
|
// 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)
|
return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
names := make([]string, len(uploads))
|
changeOpts := &ChangeRepoFilesOptions{
|
||||||
infos := make([]uploadInfo, len(uploads))
|
LastCommitID: opts.LastCommitID,
|
||||||
for i, upload := range uploads {
|
OldBranch: opts.OldBranch,
|
||||||
// Check file is not lfs locked, will return nil if lock setting not enabled
|
NewBranch: opts.NewBranch,
|
||||||
filepath := path.Join(opts.TreePath, upload.Name)
|
Message: opts.Message,
|
||||||
lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
|
Signoff: opts.Signoff,
|
||||||
if err != nil {
|
Author: opts.Author,
|
||||||
return err
|
Committer: opts.Committer,
|
||||||
}
|
|
||||||
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}
|
|
||||||
}
|
}
|
||||||
|
for _, upload := range uploads {
|
||||||
t, err := NewTemporaryUploadRepository(repo)
|
changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{
|
||||||
if err != nil {
|
Operation: "upload",
|
||||||
return err
|
TreePath: path.Join(opts.TreePath, upload.Name),
|
||||||
}
|
ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()},
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy uploaded files into repository.
|
_, err = ChangeRepoFiles(ctx, repo, doer, changeOpts)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := repo_model.DeleteUploads(ctx, uploads...); err != nil {
|
||||||
// Now commit the tree
|
log.Error("DeleteUploads: %v", err)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,13 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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}}
|
{{.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}}">
|
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
<div class="breadcrumb">
|
||||||
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
|
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}}
|
||||||
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
|
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}}
|
||||||
{{if eq .CherryPickType "revert"}}
|
{{if eq .CherryPickType "revert"}}
|
||||||
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
|
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<div class="commit-form-wrapper">
|
<div class="commit-form-wrapper">
|
||||||
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
||||||
<div class="commit-form">
|
<div class="commit-form">
|
||||||
<h3>{{- if .CanCommitToBranch.WillSign}}
|
<h3>{{- if .CommitFormBehaviors.WillSign}}
|
||||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||||
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
||||||
{{- else}}
|
{{- 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"}}
|
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
|
||||||
{{- end}}</h3>
|
{{- end}}</h3>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -22,17 +22,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="quick-pull-choice js-quick-pull-choice">
|
<div class="quick-pull-choice js-quick-pull-choice">
|
||||||
<div class="field">
|
<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}}>
|
<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>
|
<label>
|
||||||
{{svg "octicon-git-commit"}}
|
{{svg "octicon-git-commit"}}
|
||||||
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
|
{{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">
|
<div class="ui visible small warning message">
|
||||||
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
||||||
<ul>
|
<ul>
|
||||||
{{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
{{if not .CommitFormBehaviors.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 and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -42,14 +42,14 @@
|
|||||||
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<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}}>
|
<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}}
|
{{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}}>
|
<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}}
|
{{end}}
|
||||||
<label>
|
<label>
|
||||||
{{svg "octicon-git-pull-request"}}
|
{{svg "octicon-git-pull-request"}}
|
||||||
{{if .CanCreatePullRequest}}
|
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||||
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
|
<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"}}
|
{{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"}}">
|
<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>
|
<span class="text-muted js-quick-pull-normalization-info"></span>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
|
{{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>
|
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
|
||||||
<select class="ui selection dropdown" name="commit_email">
|
<select class="ui selection dropdown" name="commit_email">
|
||||||
{{- range $email := .CommitCandidateEmails -}}
|
{{- range $email := .CommitCandidateEmails -}}
|
||||||
@ -77,7 +77,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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}}
|
{{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>
|
</button>
|
||||||
<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>
|
<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" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<form class="ui form" method="post">
|
<form class="ui form form-fetch-action" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
|
||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,30 +3,13 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||||
>
|
>
|
||||||
{{.CsrfTokenHtml}}
|
{{.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="repo-editor-header">
|
||||||
<div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}">
|
{{template "repo/editor/common_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 "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>
|
|
||||||
</div>
|
</div>
|
||||||
{{if not .NotEditableReason}}
|
{{if not .NotEditableReason}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -3,15 +3,13 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||||
>
|
>
|
||||||
{{.CsrfTokenHtml}}
|
{{.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="repo-editor-header">
|
||||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
<div class="breadcrumb">
|
||||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||||
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
|
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
|
||||||
<div class="breadcrumb-divider">:</div>
|
<div class="breadcrumb-divider">:</div>
|
||||||
|
@ -3,25 +3,10 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<form class="ui comment form" method="post">
|
<form class="ui comment form form-fetch-action" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
|
{{template "repo/editor/common_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 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{template "repo/upload" .}}
|
{{template "repo/upload" .}}
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
{{if not $isTreePathRoot}}
|
{{if not $isTreePathRoot}}
|
||||||
{{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}}
|
{{$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>
|
<a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
|
||||||
{{- range $i, $v := .TreeNames -}}
|
{{- range $i, $v := .TreeNames -}}
|
||||||
<span class="breadcrumb-divider">/</span>
|
<span class="breadcrumb-divider">/</span>
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) {
|
|||||||
"content": "package main",
|
"content": "package main",
|
||||||
"commit_choice": "direct",
|
"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
|
// let gitea calculate language stats
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
@ -12,12 +12,13 @@ import (
|
|||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testLicenseContent = `
|
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:
|
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,
|
"content": testLicenseContent,
|
||||||
"commit_choice": "direct",
|
"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
|
// let gitea update repo license
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/tests"
|
"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
|
// Request editor page
|
||||||
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
|
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
|
||||||
req := NewRequest(t, "GET", newURL)
|
req := NewRequest(t, "GET", newURL)
|
||||||
@ -52,7 +53,8 @@ func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, file
|
|||||||
"content": content,
|
"content": content,
|
||||||
"commit_choice": "direct",
|
"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) {
|
func TestCreateFileOnProtectedBranch(t *testing.T) {
|
||||||
@ -88,9 +90,9 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
|
|||||||
"commit_choice": "direct",
|
"commit_choice": "direct",
|
||||||
})
|
})
|
||||||
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
// Check body for error message
|
respErr := test.ParseJSONError(resp.Body.Bytes())
|
||||||
assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".")
|
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
|
||||||
|
|
||||||
// remove the protected branch
|
// remove the protected branch
|
||||||
csrf = GetUserCSRFToken(t, session)
|
csrf = GetUserCSRFToken(t, session)
|
||||||
@ -131,7 +133,8 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
|
|||||||
"commit_choice": "direct",
|
"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
|
// Verify the change
|
||||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
|
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,
|
"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
|
// Verify the change
|
||||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
|
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
|
||||||
@ -211,9 +215,8 @@ func TestWebGitCommitEmail(t *testing.T) {
|
|||||||
newCommit := getLastCommit(t)
|
newCommit := getLastCommit(t)
|
||||||
if expectedUserName == "" {
|
if expectedUserName == "" {
|
||||||
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
|
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
respErr := test.ParseJSONError(resp.Body.Bytes())
|
||||||
errMsg := htmlDoc.doc.Find(".ui.negative.message").Text()
|
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
|
||||||
assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"))
|
|
||||||
} else {
|
} else {
|
||||||
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
|
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||||
assert.Equal(t, expectedUserName, newCommit.Author.Name)
|
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
|
// 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"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"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)
|
url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
|
||||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||||
"_csrf": GetUserCSRFToken(t, session),
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
@ -38,7 +37,8 @@ func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, tree
|
|||||||
"tree_path": treePath,
|
"tree_path": treePath,
|
||||||
"content": content,
|
"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) {
|
func TestEmptyRepo(t *testing.T) {
|
||||||
@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) {
|
|||||||
"content": "newly-added-test-file",
|
"content": "newly-added-test-file",
|
||||||
})
|
})
|
||||||
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
redirect := test.RedirectURL(resp)
|
redirect := test.RedirectURL(resp)
|
||||||
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect)
|
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"],
|
"files": respMap["uuid"],
|
||||||
"tree_path": "",
|
"tree_path": "",
|
||||||
})
|
})
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
redirect := test.RedirectURL(resp)
|
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)
|
req = NewRequest(t, "GET", redirect)
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -159,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) {
|
|||||||
"commit_summary": "user2 updated the file",
|
"commit_summary": "user2 updated the file",
|
||||||
"commit_choice": "direct",
|
"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 {
|
.breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb .breadcrumb-divider {
|
.breadcrumb .breadcrumb-divider {
|
||||||
color: var(--color-text-light-2);
|
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 {
|
.repository.file.list .non-diff-file-content .header .icon {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formMethod = formEl.getAttribute('method') || 'get';
|
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 formData = new FormData(formEl);
|
||||||
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
|
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
|
||||||
if (submitterName) {
|
if (submitterName) {
|
||||||
|
@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts';
|
|||||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||||
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {submitFormFetchAction} from './common-fetch-action.ts';
|
||||||
|
|
||||||
function initEditPreviewTab(elForm: HTMLFormElement) {
|
function initEditPreviewTab(elForm: HTMLFormElement) {
|
||||||
const elTabMenu = elForm.querySelector('.repo-editor-menu');
|
const elTabMenu = elForm.querySelector('.repo-editor-menu');
|
||||||
@ -143,31 +144,28 @@ export function initRepoEditor() {
|
|||||||
|
|
||||||
const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
|
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
|
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
|
||||||
// to enable or disable the commit button
|
// to enable or disable the commit button
|
||||||
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
|
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
|
||||||
const dirtyFileClass = 'dirty-file';
|
const dirtyFileClass = 'dirty-file';
|
||||||
|
|
||||||
// Enabling the button at the start if the page has posted
|
const syncCommitButtonState = () => {
|
||||||
if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') {
|
const dirty = elForm.classList.contains(dirtyFileClass);
|
||||||
commitButton.disabled = false;
|
commitButton.disabled = !dirty;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Registering a custom listener for the file path and the file content
|
// 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
|
// FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
|
||||||
applyAreYouSure(elForm, {
|
applyAreYouSure(elForm, {
|
||||||
silent: true,
|
silent: true,
|
||||||
dirtyClass: dirtyFileClass,
|
dirtyClass: dirtyFileClass,
|
||||||
fieldSelector: ':input:not(.commit-form-wrapper :input)',
|
fieldSelector: ':input:not(.commit-form-wrapper :input)',
|
||||||
change($form: any) {
|
change: syncCommitButtonState,
|
||||||
const dirty = $form[0]?.classList.contains(dirtyFileClass);
|
|
||||||
commitButton.disabled = !dirty;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
syncCommitButtonState(); // disable the "commit" button when no content changes
|
||||||
// on the upload page, there is no editor(textarea)
|
|
||||||
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
|
|
||||||
if (!editArea) return;
|
|
||||||
|
|
||||||
initEditPreviewTab(elForm);
|
initEditPreviewTab(elForm);
|
||||||
|
|
||||||
@ -182,7 +180,7 @@ export function initRepoEditor() {
|
|||||||
editor.setValue(value);
|
editor.setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
commitButton?.addEventListener('click', async (e) => {
|
commitButton.addEventListener('click', async (e) => {
|
||||||
// A modal which asks if an empty file should be committed
|
// A modal which asks if an empty file should be committed
|
||||||
if (!editArea.value) {
|
if (!editArea.value) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -191,7 +189,7 @@ export function initRepoEditor() {
|
|||||||
content: elForm.getAttribute('data-text-empty-confirm-content'),
|
content: elForm.getAttribute('data-text-empty-confirm-content'),
|
||||||
})) {
|
})) {
|
||||||
ignoreAreYouSure(elForm);
|
ignoreAreYouSure(elForm);
|
||||||
elForm.submit();
|
submitFormFetchAction(elForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user