mirror of
https://github.com/go-gitea/gitea.git
synced 2025-06-24 19:24:07 +02:00
Edit file workflow for creating a fork and proposing changes (#34240)
When viewing a file that the user can't edit because they can't write to the branch, the new, upload, patch, edit and delete functionality is no longer disabled. If no user fork of the repository exists, there is now a page to create one. It will automatically create a fork with a single branch matching the one being viewed, and a unique repository name will be automatically picked. When a fork exists, but it's archived, a mirror or the user can't write code to it, there will instead be a message explaining the situation. If the usable fork exists, a message will appear at the top of the edit page explaining that the changes will be applied to a branch in the fork. The base repository branch will be pushed to a new branch to the fork, and then the edits will be applied on top. The suggestion to fork happens when accessing /_edit/, so that for example online documentation can have an "edit this page" link to the base repository that does the right thing. Also includes changes to properly report errors when trying to commit to a new branch that is protected, and when trying to commit to an existing branch when choosing the new branch option. Resolves #9017, #20882 --------- Co-authored-by: Brecht Van Lommel <brecht@blender.org> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
1748045285
commit
a46b16f10f
@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
|
|||||||
|
|
||||||
// CanEnableEditor returns true if repository meets the requirements of web editor.
|
// CanEnableEditor returns true if repository meets the requirements of web editor.
|
||||||
func (repo *Repository) CanEnableEditor() bool {
|
func (repo *Repository) CanEnableEditor() bool {
|
||||||
return !repo.IsMirror
|
return !repo.IsMirror && !repo.IsArchived
|
||||||
}
|
}
|
||||||
|
|
||||||
// DescriptionHTML does special handles to description and return HTML string.
|
// DescriptionHTML does special handles to description and return HTML string.
|
||||||
|
@ -1399,6 +1399,13 @@ editor.revert = Revert %s onto:
|
|||||||
editor.failed_to_commit = Failed to commit changes.
|
editor.failed_to_commit = Failed to commit changes.
|
||||||
editor.failed_to_commit_summary = Error Message:
|
editor.failed_to_commit_summary = Error Message:
|
||||||
|
|
||||||
|
editor.fork_create = Fork Repository to Propose Changes
|
||||||
|
editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request.
|
||||||
|
editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request.
|
||||||
|
editor.fork_not_editable = You have forked this repository but your fork is not editable.
|
||||||
|
editor.fork_failed_to_push_branch = Failed to push branch %s to your repository.
|
||||||
|
editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name.
|
||||||
|
|
||||||
commits.desc = Browse source code change history.
|
commits.desc = Browse source code change history.
|
||||||
commits.commits = Commits
|
commits.commits = Commits
|
||||||
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
|
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
@ -39,7 +40,7 @@ const (
|
|||||||
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
|
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
||||||
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||||
if cleanedTreePath != ctx.Repo.TreePath {
|
if cleanedTreePath != ctx.Repo.TreePath {
|
||||||
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
|
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
|
||||||
@ -47,18 +48,28 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
|
|||||||
redirectTo += "?" + ctx.Req.URL.RawQuery
|
redirectTo += "?" + ctx.Req.URL.RawQuery
|
||||||
}
|
}
|
||||||
ctx.Redirect(redirectTo)
|
ctx.Redirect(redirectTo)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
|
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("PrepareCommitFormBehaviors", err)
|
ctx.ServerError("PrepareCommitFormOptions", err)
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if commitFormOptions.NeedFork {
|
||||||
|
ForkToEdit(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
|
||||||
|
ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
|
||||||
|
ctx.NotFound(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||||
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
||||||
ctx.Data["CommitFormBehaviors"] = commitFormBehaviors
|
ctx.Data["CommitFormOptions"] = commitFormOptions
|
||||||
|
|
||||||
// for online editor
|
// for online editor
|
||||||
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
||||||
@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
|
|||||||
// form fields
|
// form fields
|
||||||
ctx.Data["commit_summary"] = ""
|
ctx.Data["commit_summary"] = ""
|
||||||
ctx.Data["commit_message"] = ""
|
ctx.Data["commit_message"] = ""
|
||||||
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
||||||
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
|
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
|
||||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
|
return commitFormOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
|
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
|
||||||
// show the tree path fields in the "breadcrumb" and help users to edit the target tree path
|
// show the tree path fields in the "breadcrumb" and help users to edit the target tree path
|
||||||
ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(treePath)
|
ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
type parsedEditorCommitForm[T any] struct {
|
type preparedEditorCommitForm[T any] struct {
|
||||||
form T
|
form T
|
||||||
commonForm *forms.CommitCommonForm
|
commonForm *forms.CommitCommonForm
|
||||||
CommitFormBehaviors *context.CommitFormBehaviors
|
CommitFormOptions *context.CommitFormOptions
|
||||||
TargetBranchName string
|
OldBranchName string
|
||||||
GitCommitter *files_service.IdentityOptions
|
NewBranchName string
|
||||||
|
GitCommitter *files_service.IdentityOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
|
func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
|
||||||
commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
|
commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
|
||||||
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
|
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
|
||||||
commitMessage += "\n\n" + body
|
commitMessage += "\n\n" + body
|
||||||
@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string
|
|||||||
return commitMessage
|
return commitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] {
|
func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
|
||||||
form := web.GetForm(ctx).(T)
|
form := web.GetForm(ctx).(T)
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
|||||||
commonForm := form.GetCommitCommonForm()
|
commonForm := form.GetCommitCommonForm()
|
||||||
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
|
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
|
||||||
|
|
||||||
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
|
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("PrepareCommitFormBehaviors", err)
|
ctx.ServerError("PrepareCommitFormOptions", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if commitFormOptions.NeedFork {
|
||||||
|
// It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
|
||||||
|
ctx.JSONError(ctx.Locale.TrString("error.not_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check commit behavior
|
// check commit behavior
|
||||||
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
|
fromBaseBranch := ctx.FormString("from_base_branch")
|
||||||
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
|
commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
|
||||||
|
targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
|
||||||
|
if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
|
||||||
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
|
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &parsedEditorCommitForm[T]{
|
if commitToNewBranch {
|
||||||
form: form,
|
// if target branch exists, we should stop
|
||||||
commonForm: commonForm,
|
targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
|
||||||
CommitFormBehaviors: commitFormBehaviors,
|
if err != nil {
|
||||||
TargetBranchName: targetBranchName,
|
ctx.ServerError("IsBranchExist", err)
|
||||||
GitCommitter: gitCommitter,
|
return nil
|
||||||
|
} else if targetBranchExists {
|
||||||
|
if fromBaseBranch != "" {
|
||||||
|
ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
|
||||||
|
} else {
|
||||||
|
ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldBranchName := ctx.Repo.BranchName
|
||||||
|
if fromBaseBranch != "" {
|
||||||
|
err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
|
||||||
|
ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
|
||||||
|
oldBranchName = targetBranchName
|
||||||
|
}
|
||||||
|
|
||||||
|
return &preparedEditorCommitForm[T]{
|
||||||
|
form: form,
|
||||||
|
commonForm: commonForm,
|
||||||
|
CommitFormOptions: commitFormOptions,
|
||||||
|
OldBranchName: oldBranchName,
|
||||||
|
NewBranchName: targetBranchName,
|
||||||
|
GitCommitter: gitCommitter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirectForCommitChoice redirects after committing the edit to a branch
|
// redirectForCommitChoice redirects after committing the edit to a branch
|
||||||
func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) {
|
func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
|
||||||
|
// when editing a file in a PR, it should return to the origin location
|
||||||
|
if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
|
||||||
|
ctx.JSONRedirect(returnURI)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
|
if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
|
||||||
// Redirect to a pull request when possible
|
// Redirect to a pull request when possible
|
||||||
redirectToPullRequest := false
|
redirectToPullRequest := false
|
||||||
repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
|
repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
|
||||||
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
|
||||||
redirectToPullRequest = true
|
|
||||||
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
|
|
||||||
redirectToPullRequest = true
|
redirectToPullRequest = true
|
||||||
baseBranch = repo.BaseRepo.DefaultBranch
|
baseBranch = repo.BaseRepo.DefaultBranch
|
||||||
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
||||||
repo = repo.BaseRepo
|
repo = repo.BaseRepo
|
||||||
|
} else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||||
|
redirectToPullRequest = true
|
||||||
}
|
}
|
||||||
if redirectToPullRequest {
|
if redirectToPullRequest {
|
||||||
ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
|
ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
|
||||||
@ -154,11 +209,9 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
returnURI := ctx.FormString("return_uri")
|
// redirect to the newly updated file
|
||||||
if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
|
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
|
||||||
returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath))
|
ctx.JSONRedirect(redirectTo)
|
||||||
}
|
|
||||||
ctx.JSONRedirect(returnURI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
|
func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
|
||||||
@ -268,7 +321,7 @@ func EditFile(ctx *context.Context) {
|
|||||||
func EditFilePost(ctx *context.Context) {
|
func EditFilePost(ctx *context.Context) {
|
||||||
editorAction := ctx.PathParam("editor_action")
|
editorAction := ctx.PathParam("editor_action")
|
||||||
isNewFile := editorAction == "_new"
|
isNewFile := editorAction == "_new"
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) {
|
|||||||
|
|
||||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Files: []*files_service.ChangeRepoFile{
|
Files: []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) {
|
|||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) {
|
|||||||
|
|
||||||
// DeleteFilePost response for deleting file
|
// DeleteFilePost response for deleting file
|
||||||
func DeleteFilePost(ctx *context.Context) {
|
func DeleteFilePost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
treePath := ctx.Repo.TreePath
|
treePath := ctx.Repo.TreePath
|
||||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Files: []*files_service.ChangeRepoFile{
|
Files: []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
Operation: "delete",
|
Operation: "delete",
|
||||||
@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.TargetBranchName, treePath)
|
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||||
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFile(ctx *context.Context) {
|
func UploadFile(ctx *context.Context) {
|
||||||
ctx.Data["PageIsUpload"] = true
|
ctx.Data["PageIsUpload"] = true
|
||||||
upload.AddUploadContext(ctx, "repo")
|
|
||||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||||
|
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
||||||
prepareEditorCommitFormOptions(ctx, "_upload")
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplUploadFile)
|
ctx.HTML(http.StatusOK, tplUploadFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFilePost(ctx *context.Context) {
|
func UploadFilePost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -379,8 +432,8 @@ func UploadFilePost(ctx *context.Context) {
|
|||||||
defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
|
defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
|
||||||
err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
|
err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
TreePath: parsed.form.TreePath,
|
TreePath: parsed.form.TreePath,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Files: parsed.form.Files,
|
Files: parsed.form.Files,
|
||||||
@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) {
|
|||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||||
|
@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
|
|||||||
|
|
||||||
// NewDiffPatchPost response for sending patch page
|
// NewDiffPatchPost response for sending patch page
|
||||||
func NewDiffPatchPost(ctx *context.Context) {
|
func NewDiffPatchPost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) {
|
|||||||
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
||||||
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
||||||
Author: parsed.GitCommitter,
|
Author: parsed.GitCommitter,
|
||||||
@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) {
|
|||||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||||
|
@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
|
|||||||
|
|
||||||
func CherryPickPost(ctx *context.Context) {
|
func CherryPickPost(ctx *context.Context) {
|
||||||
fromCommitID := ctx.PathParam("sha")
|
fromCommitID := ctx.PathParam("sha")
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -53,8 +53,8 @@ func CherryPickPost(ctx *context.Context) {
|
|||||||
defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
|
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{
|
opts := &files.ApplyDiffPatchOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Author: parsed.GitCommitter,
|
Author: parsed.GitCommitter,
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
routers/web/repo/editor_fork.go
Normal file
31
routers/web/repo/editor_fork.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplEditorFork templates.TplName = "repo/editor/fork"
|
||||||
|
|
||||||
|
func ForkToEdit(ctx *context.Context) {
|
||||||
|
ctx.HTML(http.StatusOK, tplEditorFork)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForkToEditPost(ctx *context.Context) {
|
||||||
|
ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
|
||||||
|
BaseRepo: ctx.Repo.Repository,
|
||||||
|
Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
|
||||||
|
Description: ctx.Repo.Repository.Description,
|
||||||
|
SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork?
|
||||||
|
})
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSONRedirect("") // reload the page, the new fork should be editable now
|
||||||
|
}
|
@ -11,9 +11,11 @@ import (
|
|||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
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"
|
||||||
"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/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
context_service "code.gitea.io/gitea/services/context"
|
context_service "code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
|||||||
}
|
}
|
||||||
return treeNames, treePaths
|
return treeNames, treePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getUniqueRepositoryName Gets a unique repository name for a user
|
||||||
|
// It will append a -<num> postfix if the name is already taken
|
||||||
|
func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
|
||||||
|
uniqueName := name
|
||||||
|
for i := 1; i < 1000; i++ {
|
||||||
|
_, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
|
||||||
|
if err != nil || repo_model.IsErrRepoNotExist(err) {
|
||||||
|
return uniqueName
|
||||||
|
}
|
||||||
|
uniqueName = fmt.Sprintf("%s-%d", name, i)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
|
||||||
|
return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
|
||||||
|
Remote: targetRepo.RepoPath(),
|
||||||
|
Branch: baseBranchName + ":" + targetBranchName,
|
||||||
|
Env: repo_module.PushingEnvironment(doer, targetRepo),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
|
repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
|
||||||
BaseRepo: forkRepo,
|
BaseRepo: forkRepo,
|
||||||
Name: form.RepoName,
|
Name: form.RepoName,
|
||||||
Description: form.Description,
|
Description: form.Description,
|
||||||
SingleBranch: form.ForkSingleBranch,
|
SingleBranch: form.ForkSingleBranch,
|
||||||
})
|
})
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
|
||||||
|
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Data["Err_RepoName"] = true
|
ctx.Data["Err_RepoName"] = true
|
||||||
switch {
|
switch {
|
||||||
case repo_model.IsErrReachLimitOfRepo(err):
|
case repo_model.IsErrReachLimitOfRepo(err):
|
||||||
maxCreationLimit := ctxUser.MaxCreationLimit()
|
maxCreationLimit := owner.MaxCreationLimit()
|
||||||
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
|
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
|
||||||
ctx.JSONError(msg)
|
ctx.JSONError(msg)
|
||||||
case repo_model.IsErrRepoAlreadyExist(err):
|
case repo_model.IsErrRepoAlreadyExist(err):
|
||||||
@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
|
|||||||
default:
|
default:
|
||||||
ctx.ServerError("ForkPost", err)
|
ctx.ServerError("ForkPost", err)
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
return repo
|
||||||
log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
|
|
||||||
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
|
|
||||||
}
|
}
|
||||||
|
@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||||||
|
|
||||||
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||||
// archived or mirror repository, the buttons should not be shown
|
// archived or mirror repository, the buttons should not be shown
|
||||||
if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() {
|
if !ctx.Repo.Repository.CanEnableEditor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
||||||
|
ctx.Data["CanEditFile"] = true
|
||||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
|
||||||
|
ctx.Data["CanDeleteFile"] = true
|
||||||
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
|||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() {
|
||||||
ctx.Data["CanEditReadmeFile"] = true
|
ctx.Data["CanEditReadmeFile"] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1313,23 +1313,35 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
||||||
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
|
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
|
||||||
|
|
||||||
m.Group("/{username}/{reponame}", func() { // repo code
|
m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/_preview/*", repo.DiffPreviewPost)
|
// "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
|
||||||
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
|
// Because reader can "fork and edit"
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
canWriteToBranch := context.CanWriteToBranch()
|
||||||
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
|
m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
|
||||||
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
|
||||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
// the path params are used in PrepareCommitFormOptions to construct the correct form action URL
|
||||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
m.Combo("/{editor_action:_edit}/*").
|
||||||
Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
|
Get(repo.EditFile).
|
||||||
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
|
m.Combo("/{editor_action:_new}/*").
|
||||||
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
|
Get(repo.EditFile).
|
||||||
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||||
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
m.Combo("/{editor_action:_delete}/*").
|
||||||
|
Get(repo.DeleteFile).
|
||||||
|
Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
|
||||||
|
m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
|
||||||
|
Get(repo.UploadFile).
|
||||||
|
Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
|
||||||
|
m.Combo("/{editor_action:_diffpatch}/*").
|
||||||
|
Get(repo.NewDiffPatch).
|
||||||
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
|
||||||
|
m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
|
||||||
|
Get(repo.CherryPick).
|
||||||
|
Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
|
||||||
|
}, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/upload-file", repo.UploadFileToServer)
|
m.Post("/upload-file", repo.UploadFileToServer)
|
||||||
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
||||||
|
@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User
|
|||||||
return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
|
return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanEnableEditor returns true if repository is editable and user has proper access level.
|
|
||||||
func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool {
|
|
||||||
return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanCreateBranch returns true if repository is editable and user has proper access level.
|
// CanCreateBranch returns true if repository is editable and user has proper access level.
|
||||||
func (r *Repository) CanCreateBranch() bool {
|
func (r *Repository) CanCreateBranch() bool {
|
||||||
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
|
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
|
||||||
@ -94,9 +89,13 @@ func RepoMustNotBeArchived() func(ctx *Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommitFormBehaviors struct {
|
type CommitFormOptions struct {
|
||||||
|
NeedFork bool
|
||||||
|
|
||||||
|
TargetRepo *repo_model.Repository
|
||||||
|
TargetFormAction string
|
||||||
|
WillSubmitToFork bool
|
||||||
CanCommitToBranch bool
|
CanCommitToBranch bool
|
||||||
EditorEnabled bool
|
|
||||||
UserCanPush bool
|
UserCanPush bool
|
||||||
RequireSigned bool
|
RequireSigned bool
|
||||||
WillSign bool
|
WillSign bool
|
||||||
@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
|
|||||||
CanCreateBasePullRequest bool
|
CanCreateBasePullRequest bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
|
func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
|
||||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
if !refName.IsBranch() {
|
||||||
|
// it shouldn't happen because middleware already checks
|
||||||
|
return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
|
||||||
|
}
|
||||||
|
|
||||||
|
originRepo := targetRepo
|
||||||
|
branchName := refName.ShortName()
|
||||||
|
// TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does
|
||||||
|
if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) {
|
||||||
|
targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID)
|
||||||
|
if targetRepo == nil {
|
||||||
|
return &CommitFormOptions{NeedFork: true}, nil
|
||||||
|
}
|
||||||
|
// now, we get our own forked repo; it must be writable by us.
|
||||||
|
}
|
||||||
|
submitToForkedRepo := targetRepo.ID != originRepo.ID
|
||||||
|
err := targetRepo.GetBaseRepo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
userCanPush := true
|
|
||||||
requireSigned := false
|
|
||||||
if protectedBranch != nil {
|
|
||||||
protectedBranch.Repo = r.Repository
|
|
||||||
userCanPush = protectedBranch.CanUserPush(ctx, doer)
|
|
||||||
requireSigned = protectedBranch.RequireSignedCommits
|
|
||||||
}
|
|
||||||
|
|
||||||
sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
|
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
|
||||||
|
|
||||||
canEnableEditor := r.CanEnableEditor(ctx, doer)
|
|
||||||
canCommit := canEnableEditor && userCanPush
|
|
||||||
if requireSigned {
|
|
||||||
canCommit = canCommit && sign
|
|
||||||
}
|
|
||||||
wontSignReason := ""
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if asymkey_service.IsErrWontSign(err) {
|
return nil, err
|
||||||
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
}
|
||||||
err = nil
|
canPushWithProtection := true
|
||||||
} else {
|
protectionRequireSigned := false
|
||||||
wontSignReason = "error"
|
if protectedBranch != nil {
|
||||||
}
|
protectedBranch.Repo = targetRepo
|
||||||
|
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
|
||||||
|
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
||||||
}
|
}
|
||||||
|
|
||||||
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
|
||||||
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
|
wontSignReason := ""
|
||||||
|
if asymkey_service.IsErrWontSign(err) {
|
||||||
|
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &CommitFormBehaviors{
|
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
|
||||||
CanCommitToBranch: canCommit,
|
if protectionRequireSigned {
|
||||||
EditorEnabled: canEnableEditor,
|
canCommitToBranch = canCommitToBranch && willSign
|
||||||
UserCanPush: userCanPush,
|
}
|
||||||
RequireSigned: requireSigned,
|
|
||||||
WillSign: sign,
|
canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
||||||
SigningKey: keyID,
|
canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
|
||||||
|
|
||||||
|
opts := &CommitFormOptions{
|
||||||
|
TargetRepo: targetRepo,
|
||||||
|
WillSubmitToFork: submitToForkedRepo,
|
||||||
|
CanCommitToBranch: canCommitToBranch,
|
||||||
|
UserCanPush: canPushWithProtection,
|
||||||
|
RequireSigned: protectionRequireSigned,
|
||||||
|
WillSign: willSign,
|
||||||
|
SigningKey: signKeyID,
|
||||||
WontSignReason: wontSignReason,
|
WontSignReason: wontSignReason,
|
||||||
|
|
||||||
CanCreatePullRequest: canCreatePullRequest,
|
CanCreatePullRequest: canCreatePullRequest,
|
||||||
CanCreateBasePullRequest: canCreateBasePullRequest,
|
CanCreateBasePullRequest: canCreateBasePullRequest,
|
||||||
}, err
|
}
|
||||||
|
editorAction := ctx.PathParam("editor_action")
|
||||||
|
editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
if submitToForkedRepo {
|
||||||
|
// there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo
|
||||||
|
editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName)
|
||||||
|
}
|
||||||
|
if editorAction == "_cherrypick" {
|
||||||
|
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining
|
||||||
|
} else {
|
||||||
|
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining
|
||||||
|
}
|
||||||
|
if ctx.Req.URL.RawQuery != "" {
|
||||||
|
opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanUseTimetracker returns whether a user can use the timetracker.
|
// CanUseTimetracker returns whether a user can use the timetracker.
|
||||||
|
@ -11,7 +11,9 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/reqctx"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
@ -106,14 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
|
|||||||
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
|
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
|
||||||
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
||||||
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
|
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
|
||||||
case "repo":
|
|
||||||
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
|
|
||||||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
|
|
||||||
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
|
|
||||||
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
|
|
||||||
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
|
|
||||||
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
|
|
||||||
default:
|
default:
|
||||||
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
|
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) {
|
||||||
|
ctxData, repoLink := ctx.GetData(), repo.Link()
|
||||||
|
ctxData["UploadUrl"] = repoLink + "/upload-file"
|
||||||
|
ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove"
|
||||||
|
ctxData["UploadLinkUrl"] = repoLink + "/upload-file"
|
||||||
|
ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
|
||||||
|
ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
|
||||||
|
ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
|
||||||
|
}
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
{{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 form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
|
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<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="breadcrumb">
|
<div class="breadcrumb">
|
||||||
|
@ -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 .CommitFormBehaviors.WillSign}}
|
<h3>{{- if .CommitFormOptions.WillSign}}
|
||||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.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" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
|
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.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 .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
|
<div class="ui radio checkbox {{if not .CommitFormOptions.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 .CommitFormBehaviors.CanCommitToBranch}}
|
{{if not .CommitFormOptions.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 .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
{{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||||
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
{{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.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 .CommitFormBehaviors.CanCreatePullRequest}}
|
{{if .CommitFormOptions.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 .CommitFormBehaviors.CanCreatePullRequest}}
|
{{if .CommitFormOptions.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"}}
|
||||||
|
6
templates/repo/editor/common_top.tmpl
Normal file
6
templates/repo/editor/common_top.tmpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{{if .CommitFormOptions.WillSubmitToFork}}
|
||||||
|
<div class="ui blue message">
|
||||||
|
{{$repoLinkHTML := HTMLFormat `<a href="%s">%s</a>` .CommitFormOptions.TargetRepo.Link .CommitFormOptions.TargetRepo.FullName}}
|
||||||
|
{{ctx.Locale.Tr "repo.editor.fork_edit_description" $repoLinkHTML}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
@ -3,8 +3,9 @@
|
|||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<form class="ui form form-fetch-action" method="post">
|
<form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
{{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 form-fetch-action" method="post"
|
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
||||||
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}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</div>
|
||||||
|
18
templates/repo/editor/fork.tmpl
Normal file
18
templates/repo/editor/fork.tmpl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{{template "base/head" .}}
|
||||||
|
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||||
|
{{template "repo/header" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
<form class="ui form form-fetch-action" method="post" action="{{.RepoLink}}/_fork/{{.BranchName | PathEscapeSegments}}">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="tw-text-center">
|
||||||
|
<div class="tw-my-[40px]">
|
||||||
|
<h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3>
|
||||||
|
<p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "repo.fork_repo"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
@ -3,11 +3,12 @@
|
|||||||
{{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 form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
|
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
|
||||||
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}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
{{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 form-fetch-action" method="post">
|
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,8 +41,8 @@
|
|||||||
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
|
{{if and .RefFullName.IsBranch (not .IsViewFile)}}
|
||||||
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
|
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
|
||||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -19,292 +20,278 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
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/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateFile(t *testing.T) {
|
func TestEditor(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
session := loginUser(t, "user2")
|
sessionUser2 := loginUser(t, "user2")
|
||||||
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
|
t.Run("EditFileNotAllowed", testEditFileNotAllowed)
|
||||||
|
t.Run("DiffPreview", testEditorDiffPreview)
|
||||||
|
t.Run("CreateFile", testEditorCreateFile)
|
||||||
|
t.Run("EditFile", func(t *testing.T) {
|
||||||
|
testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n")
|
||||||
|
testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n")
|
||||||
|
})
|
||||||
|
t.Run("PatchFile", testEditorPatchFile)
|
||||||
|
t.Run("DeleteFile", func(t *testing.T) {
|
||||||
|
viewLink := "/user2/repo1/src/branch/branch2/README.md"
|
||||||
|
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK)
|
||||||
|
testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"})
|
||||||
|
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound)
|
||||||
|
})
|
||||||
|
t.Run("ForkToEditFile", func(t *testing.T) {
|
||||||
|
testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md")
|
||||||
|
})
|
||||||
|
t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail)
|
||||||
|
t.Run("ProtectedBranch", testEditorProtectedBranch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testEditorCreateFile(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
|
||||||
|
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
|
||||||
|
"tree_path": "test.txt",
|
||||||
|
"commit_choice": "direct",
|
||||||
|
"new_branch_name": "master",
|
||||||
|
}, `A file named "test.txt" already exists in this repository.`)
|
||||||
|
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
|
||||||
|
"tree_path": "test.txt",
|
||||||
|
"commit_choice": "commit-to-new-branch",
|
||||||
|
"new_branch_name": "master",
|
||||||
|
}, `Branch "master" already exists in this repository.`)
|
||||||
|
}
|
||||||
|
|
||||||
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
|
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
|
||||||
// Request editor page
|
testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{
|
||||||
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
|
|
||||||
req := NewRequest(t, "GET", newURL)
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
|
||||||
lastCommit := doc.GetInputValueByName("last_commit")
|
|
||||||
assert.NotEmpty(t, lastCommit)
|
|
||||||
|
|
||||||
// Save new file to master branch
|
|
||||||
req = NewRequestWithValues(t, "POST", newURL, map[string]string{
|
|
||||||
"_csrf": doc.GetCSRF(),
|
|
||||||
"last_commit": lastCommit,
|
|
||||||
"tree_path": filePath,
|
"tree_path": filePath,
|
||||||
"content": content,
|
"content": content,
|
||||||
"commit_choice": "direct",
|
"commit_choice": "direct",
|
||||||
})
|
})
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateFileOnProtectedBranch(t *testing.T) {
|
func testEditorProtectedBranch(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
session := loginUser(t, "user2")
|
||||||
session := loginUser(t, "user2")
|
// Change the "master" branch to "protected"
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
"rule_name": "master",
|
||||||
|
"enable_push": "true",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
flashMsg := session.GetCookieFlashMessage()
|
||||||
|
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
|
||||||
|
|
||||||
csrf := GetUserCSRFToken(t, session)
|
// Try to commit a file to the "master" branch and it should fail
|
||||||
// Change master branch to protected
|
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
assert.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
"_csrf": csrf,
|
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
|
||||||
"rule_name": "master",
|
}
|
||||||
"enable_push": "true",
|
|
||||||
})
|
|
||||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
||||||
// Check if master branch has been locked successfully
|
|
||||||
flashMsg := session.GetCookieFlashMessage()
|
|
||||||
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
|
|
||||||
|
|
||||||
// Request editor page
|
func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
|
||||||
req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
|
req := NewRequest(t, "GET", requestPath)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
form := map[string]string{
|
||||||
|
"_csrf": htmlDoc.GetCSRF(),
|
||||||
|
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
|
||||||
|
}
|
||||||
|
maps.Copy(form, params)
|
||||||
|
req = NewRequestWithValues(t, "POST", requestPath, form)
|
||||||
|
return session.MakeRequest(t, req, NoExpectedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) {
|
||||||
|
resp := testEditorActionPostRequest(t, session, requestPath, params)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
|
assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder {
|
||||||
|
params["tree_path"] = util.IfZero(params["tree_path"], filePath)
|
||||||
|
newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"])
|
||||||
|
resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||||
|
req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Equal(t, params["content"], resp.Body.String())
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) {
|
||||||
|
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
|
||||||
|
"content": newContent,
|
||||||
|
"commit_choice": "direct",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) {
|
||||||
|
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
|
||||||
|
"content": newContent,
|
||||||
|
"commit_choice": "commit-to-new-branch",
|
||||||
|
"new_branch_name": targetBranch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditorDiffPreview(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
"content": "Hello, World (Edited)\n",
|
||||||
|
})
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditorPatchFile(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..aaaaaaaaaa
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/patch-file-1.txt
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+`
|
||||||
|
testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{
|
||||||
|
"content": pathContentCommon + "patched content\n",
|
||||||
|
"commit_choice": "commit-to-new-branch",
|
||||||
|
"new_branch_name": "patched-branch",
|
||||||
|
})
|
||||||
|
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK)
|
||||||
|
assert.Equal(t, "patched content\n", resp.Body.String())
|
||||||
|
|
||||||
|
// patch again, it should fail
|
||||||
|
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{
|
||||||
|
"content": pathContentCommon + "another patched content\n",
|
||||||
|
"commit_choice": "commit-to-new-branch",
|
||||||
|
"new_branch_name": "patched-branch-1",
|
||||||
|
})
|
||||||
|
assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditorWebGitCommitEmail(t *testing.T) {
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
require.True(t, user.KeepEmailPrivate)
|
||||||
|
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
||||||
|
defer gitRepo.Close()
|
||||||
|
getLastCommit := func(t *testing.T) *git.Commit {
|
||||||
|
c, err := gitRepo.GetBranchCommit("master")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
|
||||||
|
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
|
||||||
|
lastCommit := getLastCommit(t)
|
||||||
|
params["_csrf"] = GetUserCSRFToken(t, session)
|
||||||
|
params["last_commit"] = lastCommit.ID.String()
|
||||||
|
params["commit_choice"] = "direct"
|
||||||
|
req := NewRequestWithValues(t, "POST", link, params)
|
||||||
|
resp := session.MakeRequest(t, req, NoExpectedStatus)
|
||||||
|
newCommit := getLastCommit(t)
|
||||||
|
if expectedUserName == "" {
|
||||||
|
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||||
|
respErr := test.ParseJSONError(resp.Body.Bytes())
|
||||||
|
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
|
||||||
|
assert.Equal(t, expectedUserName, newCommit.Author.Name)
|
||||||
|
assert.Equal(t, expectedEmail, newCommit.Author.Email)
|
||||||
|
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
|
||||||
|
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile := func(t *testing.T, name, content string) string {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
uploadForm := multipart.NewWriter(body)
|
||||||
|
file, _ := uploadForm.CreateFormFile("file", name)
|
||||||
|
_, _ = io.Copy(file, strings.NewReader(content))
|
||||||
|
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
|
||||||
|
_ = uploadForm.Close()
|
||||||
|
|
||||||
|
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
|
||||||
|
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
respMap := map[string]string{}
|
||||||
lastCommit := doc.GetInputValueByName("last_commit")
|
DecodeJSON(t, resp, &respMap)
|
||||||
assert.NotEmpty(t, lastCommit)
|
return respMap["uuid"]
|
||||||
|
}
|
||||||
|
|
||||||
// Save new file to master branch
|
t.Run("EmailInactive", func(t *testing.T) {
|
||||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
|
defer tests.PrintCurrentTest(t)()
|
||||||
"_csrf": doc.GetCSRF(),
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
|
||||||
"last_commit": lastCommit,
|
require.False(t, email.IsActivated)
|
||||||
"tree_path": "test.txt",
|
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
||||||
"content": "Content",
|
"tree_path": "README.md",
|
||||||
"commit_choice": "direct",
|
"content": "test content",
|
||||||
})
|
"commit_email": email.Email,
|
||||||
|
}, "", "")
|
||||||
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
|
||||||
respErr := test.ParseJSONError(resp.Body.Bytes())
|
|
||||||
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
|
|
||||||
|
|
||||||
// remove the protected branch
|
|
||||||
csrf = GetUserCSRFToken(t, session)
|
|
||||||
|
|
||||||
// Change master branch to protected
|
|
||||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{
|
|
||||||
"_csrf": csrf,
|
|
||||||
})
|
|
||||||
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
res := make(map[string]string)
|
|
||||||
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
|
|
||||||
assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"])
|
|
||||||
|
|
||||||
// Check if master branch has been locked successfully
|
|
||||||
flashMsg = session.GetCookieFlashMessage()
|
|
||||||
assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
|
t.Run("EmailInvalid", func(t *testing.T) {
|
||||||
// Get to the 'edit this file' page
|
defer tests.PrintCurrentTest(t)()
|
||||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
require.NotEqual(t, email.UID, user.ID)
|
||||||
|
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
"tree_path": "README.md",
|
||||||
lastCommit := htmlDoc.GetInputValueByName("last_commit")
|
"content": "test content",
|
||||||
assert.NotEmpty(t, lastCommit)
|
"commit_email": email.Email,
|
||||||
|
}, "", "")
|
||||||
// Submit the edits
|
|
||||||
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
|
|
||||||
map[string]string{
|
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
|
||||||
"last_commit": lastCommit,
|
|
||||||
"tree_path": filePath,
|
|
||||||
"content": newContent,
|
|
||||||
"commit_choice": "direct",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.Equal(t, newContent, resp.Body.String())
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder {
|
|
||||||
// Get to the 'edit this file' page
|
|
||||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
lastCommit := htmlDoc.GetInputValueByName("last_commit")
|
|
||||||
assert.NotEmpty(t, lastCommit)
|
|
||||||
|
|
||||||
// Submit the edits
|
|
||||||
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
|
|
||||||
map[string]string{
|
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
|
||||||
"last_commit": lastCommit,
|
|
||||||
"tree_path": filePath,
|
|
||||||
"content": newContent,
|
|
||||||
"commit_choice": "commit-to-new-branch",
|
|
||||||
"new_branch_name": targetBranch,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.Equal(t, newContent, resp.Body.String())
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEditFile(t *testing.T) {
|
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
||||||
session := loginUser(t, "user2")
|
|
||||||
testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestEditFileToNewBranch(t *testing.T) {
|
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
|
||||||
session := loginUser(t, "user2")
|
|
||||||
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWebGitCommitEmail(t *testing.T) {
|
|
||||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
||||||
require.True(t, user.KeepEmailPrivate)
|
|
||||||
|
|
||||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
||||||
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
|
|
||||||
defer gitRepo.Close()
|
|
||||||
getLastCommit := func(t *testing.T) *git.Commit {
|
|
||||||
c, err := gitRepo.GetBranchCommit("master")
|
|
||||||
require.NoError(t, err)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
session := loginUser(t, user.Name)
|
|
||||||
|
|
||||||
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
|
|
||||||
lastCommit := getLastCommit(t)
|
|
||||||
params["_csrf"] = GetUserCSRFToken(t, session)
|
|
||||||
params["last_commit"] = lastCommit.ID.String()
|
|
||||||
params["commit_choice"] = "direct"
|
|
||||||
req := NewRequestWithValues(t, "POST", link, params)
|
|
||||||
resp := session.MakeRequest(t, req, NoExpectedStatus)
|
|
||||||
newCommit := getLastCommit(t)
|
|
||||||
if expectedUserName == "" {
|
|
||||||
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
|
|
||||||
respErr := test.ParseJSONError(resp.Body.Bytes())
|
|
||||||
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
|
|
||||||
} else {
|
|
||||||
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
|
|
||||||
assert.Equal(t, expectedUserName, newCommit.Author.Name)
|
|
||||||
assert.Equal(t, expectedEmail, newCommit.Author.Email)
|
|
||||||
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
|
|
||||||
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFile := func(t *testing.T, name, content string) string {
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
uploadForm := multipart.NewWriter(body)
|
|
||||||
file, _ := uploadForm.CreateFormFile("file", name)
|
|
||||||
_, _ = io.Copy(file, strings.NewReader(content))
|
|
||||||
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
|
|
||||||
_ = uploadForm.Close()
|
|
||||||
|
|
||||||
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
|
|
||||||
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
respMap := map[string]string{}
|
|
||||||
DecodeJSON(t, resp, &respMap)
|
|
||||||
return respMap["uuid"]
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("EmailInactive", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
|
paramsForKeepPrivate["commit_email"] = ""
|
||||||
require.False(t, email.IsActivated)
|
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
|
||||||
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
|
||||||
"tree_path": "README.md",
|
|
||||||
"content": "test content",
|
|
||||||
"commit_email": email.Email,
|
|
||||||
}, "", "")
|
|
||||||
})
|
})
|
||||||
|
t.Run("ChooseEmail", func(t *testing.T) {
|
||||||
t.Run("EmailInvalid", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
|
paramsForChosenEmail["commit_email"] = "user2@example.com"
|
||||||
require.NotEqual(t, email.UID, user.ID)
|
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
|
||||||
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
|
||||||
"tree_path": "README.md",
|
|
||||||
"content": "test content",
|
|
||||||
"commit_email": email.Email,
|
|
||||||
}, "", "")
|
|
||||||
})
|
})
|
||||||
|
return resp1, resp2
|
||||||
|
}
|
||||||
|
|
||||||
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
|
t.Run("Edit", func(t *testing.T) {
|
||||||
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
|
testWebGit(t,
|
||||||
defer tests.PrintCurrentTest(t)()
|
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
|
||||||
paramsForKeepPrivate["commit_email"] = ""
|
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
|
||||||
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
|
)
|
||||||
})
|
})
|
||||||
t.Run("ChooseEmail", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
paramsForChosenEmail["commit_email"] = "user2@example.com"
|
|
||||||
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
|
|
||||||
})
|
|
||||||
return resp1, resp2
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Edit", func(t *testing.T) {
|
t.Run("UploadDelete", func(t *testing.T) {
|
||||||
testWebGit(t,
|
file1UUID := uploadFile(t, "file1", "File 1")
|
||||||
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
|
file2UUID := uploadFile(t, "file2", "File 2")
|
||||||
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
|
testWebGit(t,
|
||||||
)
|
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
|
||||||
})
|
"/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
|
||||||
|
)
|
||||||
|
testWebGit(t,
|
||||||
|
"/user2/repo1/_delete/master/file1", map[string]string{},
|
||||||
|
"/user2/repo1/_delete/master/file2", map[string]string{},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("UploadDelete", func(t *testing.T) {
|
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
|
||||||
file1UUID := uploadFile(t, "file1", "File 1")
|
testWebGit(t,
|
||||||
file2UUID := uploadFile(t, "file2", "File 2")
|
"/user2/repo1/_diffpatch/master", map[string]string{
|
||||||
testWebGit(t,
|
"tree_path": "__dummy__",
|
||||||
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
|
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
|
||||||
"/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
|
|
||||||
)
|
|
||||||
testWebGit(t,
|
|
||||||
"/user2/repo1/_delete/master/file1", map[string]string{},
|
|
||||||
"/user2/repo1/_delete/master/file2", map[string]string{},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
|
|
||||||
testWebGit(t,
|
|
||||||
"/user2/repo1/_diffpatch/master", map[string]string{
|
|
||||||
"tree_path": "__dummy__",
|
|
||||||
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
|
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..aaaaaaaaaa
|
index 0000000000..aaaaaaaaaa
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
@ -312,10 +299,10 @@ index 0000000000..aaaaaaaaaa
|
|||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+File 1
|
+File 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"/user2/repo1/_diffpatch/master", map[string]string{
|
"/user2/repo1/_diffpatch/master", map[string]string{
|
||||||
"tree_path": "__dummy__",
|
"tree_path": "__dummy__",
|
||||||
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
|
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..bbbbbbbbbb
|
index 0000000000..bbbbbbbbbb
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
@ -323,20 +310,146 @@ index 0000000000..bbbbbbbbbb
|
|||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+File 2
|
+File 2
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
|
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
|
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp1, _ := testWebGit(t,
|
resp1, _ := testWebGit(t,
|
||||||
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
|
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
|
||||||
"/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
|
"/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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", test.RedirectURL(resp1))
|
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) {
|
||||||
|
forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) {
|
||||||
|
// visit the base repo, see the "Add File" button
|
||||||
|
req := NewRequest(t, "GET", path.Join(owner, repo))
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1)
|
||||||
|
|
||||||
|
// attempt to edit a file, see the guideline page
|
||||||
|
req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
|
||||||
|
|
||||||
|
// fork the repository
|
||||||
|
req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)})
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ForkButArchived", func(t *testing.T) {
|
||||||
|
// Fork repository because we can't edit it
|
||||||
|
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
|
||||||
|
|
||||||
|
// Archive the repository
|
||||||
|
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
|
||||||
|
map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
"repo_name": repo,
|
||||||
|
"action": "archive",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
// Check editing archived repository is disabled
|
||||||
|
req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.")
|
||||||
|
|
||||||
|
// Unfork the repository
|
||||||
|
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
|
||||||
|
map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
"repo_name": repo,
|
||||||
|
"action": "convert_fork",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fork repository again, and check the existence of the forked repo with unique name
|
||||||
|
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
|
||||||
|
session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK)
|
||||||
|
|
||||||
|
t.Run("CheckBaseRepoForm", func(t *testing.T) {
|
||||||
|
// the base repo's edit form should have the correct action and upload links (pointing to the forked repo)
|
||||||
|
req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
uploadForm := htmlDoc.doc.Find(".form-fetch-action")
|
||||||
|
formAction := uploadForm.AttrOr("action", "")
|
||||||
|
assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction)
|
||||||
|
uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "")
|
||||||
|
assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink)
|
||||||
|
newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "")
|
||||||
|
assert.Equal(t, user+"-patch-1", newBranchName)
|
||||||
|
commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "")
|
||||||
|
assert.Equal(t, "commit-to-new-branch", commitChoice)
|
||||||
|
lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "")
|
||||||
|
assert.NotEmpty(t, lastCommit)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) {
|
||||||
|
req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath))
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
editRequestForm := map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, session),
|
||||||
|
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
|
||||||
|
"tree_path": filePath,
|
||||||
|
"content": "new content in fork",
|
||||||
|
"commit_choice": "commit-to-new-branch",
|
||||||
|
}
|
||||||
|
// change a file in the forked repo with existing branch name (should fail)
|
||||||
|
editRequestForm["new_branch_name"] = "master"
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
respJSON := test.ParseJSONError(resp.Body.Bytes())
|
||||||
|
assert.Equal(t, `Branch "master" already exists in your fork, please choose a new branch name.`, respJSON.ErrorMessage)
|
||||||
|
|
||||||
|
// change a file in the forked repo (should succeed)
|
||||||
|
newBranchName := htmlDoc.GetInputValueByName("new_branch_name")
|
||||||
|
editRequestForm["new_branch_name"] = newBranchName
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp))
|
||||||
|
|
||||||
|
// check the file in the fork's branch is changed
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "new content in fork")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEditFileNotAllowed(t *testing.T) {
|
||||||
|
sessionUser1 := loginUser(t, "user1") // admin, all access
|
||||||
|
sessionUser4 := loginUser(t, "user4")
|
||||||
|
// "_cherrypick" has a different route pattern, so skip its test
|
||||||
|
operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"}
|
||||||
|
for _, operation := range operations {
|
||||||
|
t.Run(operation, func(t *testing.T) {
|
||||||
|
// Branch does not exist
|
||||||
|
targetLink := path.Join("user2", "repo1", operation, "missing", "README.md")
|
||||||
|
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
|
||||||
|
|
||||||
|
// Private repository
|
||||||
|
targetLink = path.Join("user2", "repo2", operation, "master", "Home.md")
|
||||||
|
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK)
|
||||||
|
sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
|
||||||
|
|
||||||
|
// Empty repository
|
||||||
|
targetLink = path.Join("org41", "repo61", operation, "master", "README.md")
|
||||||
|
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string {
|
|||||||
return doc.GetInputValueByName("_csrf")
|
return doc.GetInputValueByName("_csrf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists
|
// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists
|
||||||
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
|
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
|
||||||
sel := doc.doc.Find(selector)
|
sel := doc.doc.Find(selector)
|
||||||
switch v := any(checkExists).(type) {
|
switch v := any(checkExists).(type) {
|
||||||
|
@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
|
|||||||
session := loginUser(t, "user1")
|
session := loginUser(t, "user1")
|
||||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||||
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
|
||||||
testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1")
|
testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1")
|
||||||
|
|
||||||
url := path.Join("user1", "repo1", "compare", "master...status1")
|
url := path.Join("user1", "repo1", "compare", "master...status1")
|
||||||
req := NewRequestWithValues(t, "POST", url,
|
req := NewRequestWithValues(t, "POST", url,
|
||||||
|
@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
|
|||||||
import type {RequestOpts} from '../types.ts';
|
import type {RequestOpts} from '../types.ts';
|
||||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||||
|
|
||||||
const {appSubUrl, i18n} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
|
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
|
||||||
// more details are in the backend's fetch-redirect handler
|
// more details are in the backend's fetch-redirect handler
|
||||||
@ -23,11 +23,20 @@ function fetchActionDoRedirect(redirect: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
|
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
|
||||||
|
const showErrorForResponse = (code: number, message: string) => {
|
||||||
|
showErrorToast(`Error ${code || 'request'}: ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let respStatus = 0;
|
||||||
|
let respText = '';
|
||||||
try {
|
try {
|
||||||
hideToastsAll();
|
hideToastsAll();
|
||||||
const resp = await request(url, opt);
|
const resp = await request(url, opt);
|
||||||
if (resp.status === 200) {
|
respStatus = resp.status;
|
||||||
let {redirect} = await resp.json();
|
respText = await resp.text();
|
||||||
|
const respJson = JSON.parse(respText);
|
||||||
|
if (respStatus === 200) {
|
||||||
|
let {redirect} = respJson;
|
||||||
redirect = redirect || actionElem.getAttribute('data-redirect');
|
redirect = redirect || actionElem.getAttribute('data-redirect');
|
||||||
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
|
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status >= 400 && resp.status < 500) {
|
if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
|
||||||
const data = await resp.json();
|
|
||||||
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
|
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
|
||||||
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
|
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
|
||||||
if (data.errorMessage) {
|
showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
|
||||||
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
|
|
||||||
} else {
|
|
||||||
showErrorToast(`server error: ${resp.status}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(`server error: ${resp.status}`);
|
showErrorForResponse(respStatus, respText);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name !== 'AbortError') {
|
if (e.name === 'SyntaxError') {
|
||||||
console.error('error when doRequest', e);
|
showErrorForResponse(respStatus, (respText || '').substring(0, 100));
|
||||||
showErrorToast(`${i18n.network_error} ${e}`);
|
} else if (e.name !== 'AbortError') {
|
||||||
|
console.error('fetchActionDoRequest error', e);
|
||||||
|
showErrorForResponse(respStatus, `${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actionElem.classList.remove('is-loading', 'loading-icon-2px');
|
actionElem.classList.remove('is-loading', 'loading-icon-2px');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user