Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ecff26d

Browse files
committedJun 20, 2025
fix
1 parent d462ce1 commit ecff26d

31 files changed

+774
-1148
lines changed
 

‎options/locale/locale_en-US.ini

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,17 +1374,14 @@ editor.branch_already_exists = Branch "%s" already exists in this repository.
13741374
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
13751375
editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor`
13761376
editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository.
1377-
editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository.
1378-
editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
1377+
editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository.
13791378
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
13801379
editor.file_already_exists = A file named "%s" already exists in this repository.
13811380
editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge.
13821381
editor.push_out_of_date = The push appears to be out of date.
13831382
editor.commit_empty_file_header = Commit an empty file
13841383
editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
13851384
editor.no_changes_to_show = There are no changes to show.
1386-
editor.fail_to_update_file = Failed to update/create file "%s".
1387-
editor.fail_to_update_file_summary = Error Message:
13881385
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks.
13891386
editor.push_rejected = The change was rejected by the server. Please check Git Hooks.
13901387
editor.push_rejected_summary = Full Rejection Message:
@@ -1398,6 +1395,8 @@ editor.user_no_push_to_branch = User cannot push to branch
13981395
editor.require_signed_commit = Branch requires a signed commit
13991396
editor.cherry_pick = Cherry-pick %s onto:
14001397
editor.revert = Revert %s onto:
1398+
editor.failed_to_commit = Failed to commit changes.
1399+
editor.failed_to_commit_summary = Error Message:
14011400
14021401
commits.desc = Browse source code change history.
14031402
commits.commits = Commits

‎routers/web/repo/cherry_pick.go

Lines changed: 0 additions & 193 deletions
This file was deleted.

‎routers/web/repo/editor.go

Lines changed: 182 additions & 665 deletions
Large diffs are not rendered by default.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
"strings"
9+
10+
"code.gitea.io/gitea/models/unit"
11+
"code.gitea.io/gitea/modules/util"
12+
"code.gitea.io/gitea/modules/web"
13+
"code.gitea.io/gitea/services/context"
14+
"code.gitea.io/gitea/services/forms"
15+
"code.gitea.io/gitea/services/repository/files"
16+
)
17+
18+
func NewDiffPatch(ctx *context.Context) {
19+
_ = prepareEditorCommitFormOptions(ctx, "_diffpatch")
20+
if ctx.Written() {
21+
return
22+
}
23+
ctx.Data["PageIsPatch"] = true
24+
ctx.HTML(http.StatusOK, tplPatchFile)
25+
}
26+
27+
// NewDiffPatchPost response for sending patch page
28+
func NewDiffPatchPost(ctx *context.Context) {
29+
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
30+
if ctx.HasError() {
31+
ctx.JSONError(ctx.GetErrMsg())
32+
return
33+
}
34+
35+
formOpts := prepareEditorCommitFormOptions(ctx, "_diffpatch")
36+
if ctx.Written() {
37+
return
38+
}
39+
40+
branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName)
41+
if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch {
42+
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName))
43+
return
44+
}
45+
46+
commitMessage := buildEditorCommitMessage(ctx.Locale.TrString("repo.editor.patch"), form.CommitSummary, form.CommitMessage)
47+
48+
gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
49+
if !valid {
50+
ctx.Data["Err_CommitEmail"] = true
51+
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form)
52+
return
53+
}
54+
55+
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
56+
LastCommitID: form.LastCommit,
57+
OldBranch: ctx.Repo.BranchName,
58+
NewBranch: branchName,
59+
Message: commitMessage,
60+
Content: strings.ReplaceAll(form.Content.Value(), "\r", ""),
61+
Author: gitCommitter,
62+
Committer: gitCommitter,
63+
})
64+
if err != nil {
65+
editorHandleFileOperationError(ctx, branchName, err)
66+
return
67+
}
68+
69+
if form.CommitChoice == editorCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
70+
ctx.JSONRedirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
71+
} else {
72+
ctx.JSONRedirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
73+
}
74+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"bytes"
8+
"net/http"
9+
"strings"
10+
11+
"code.gitea.io/gitea/models/unit"
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/util"
14+
"code.gitea.io/gitea/modules/web"
15+
"code.gitea.io/gitea/services/context"
16+
"code.gitea.io/gitea/services/forms"
17+
"code.gitea.io/gitea/services/repository/files"
18+
)
19+
20+
func CherryPick(ctx *context.Context) {
21+
_ = prepareEditorCommitFormOptions(ctx, "_cherrypick")
22+
if ctx.Written() {
23+
return
24+
}
25+
26+
fromCommitID := ctx.PathParam("sha")
27+
ctx.Data["FromCommitID"] = fromCommitID
28+
cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
29+
if err != nil {
30+
HandleGitError(ctx, "GetCommit", err)
31+
return
32+
}
33+
34+
if ctx.FormString("cherry-pick-type") == "revert" {
35+
ctx.Data["CherryPickType"] = "revert"
36+
ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
37+
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
38+
} else {
39+
ctx.Data["CherryPickType"] = "cherry-pick"
40+
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
41+
ctx.Data["commit_summary"] = splits[0]
42+
ctx.Data["commit_message"] = splits[1]
43+
}
44+
45+
ctx.HTML(http.StatusOK, tplCherryPick)
46+
}
47+
48+
func CherryPickPost(ctx *context.Context) {
49+
form := web.GetForm(ctx).(*forms.CherryPickForm)
50+
if ctx.HasError() {
51+
ctx.JSONError(ctx.GetErrMsg())
52+
return
53+
}
54+
55+
formOpts := prepareEditorCommitFormOptions(ctx, "_cherrypick")
56+
if ctx.Written() {
57+
return
58+
}
59+
60+
fromCommitID := ctx.PathParam("sha")
61+
62+
branchName := util.Iif(form.CommitChoice == editorCommitChoiceNewBranch, form.NewBranchName, ctx.Repo.BranchName)
63+
if branchName == ctx.Repo.BranchName && !formOpts.CommitFormBehaviors.CanCommitToBranch {
64+
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName))
65+
return
66+
}
67+
68+
defaultMessage := util.Iif(form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
69+
commitMessage := buildEditorCommitMessage(defaultMessage, form.CommitSummary, form.CommitMessage)
70+
71+
gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
72+
if !valid {
73+
ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
74+
return
75+
}
76+
77+
opts := &files.ApplyDiffPatchOptions{
78+
LastCommitID: form.LastCommit,
79+
OldBranch: ctx.Repo.BranchName,
80+
NewBranch: branchName,
81+
Message: commitMessage,
82+
Author: gitCommitter,
83+
Committer: gitCommitter,
84+
}
85+
86+
// First try the simple plain read-tree -m approach
87+
opts.Content = fromCommitID
88+
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
89+
// Drop through to the "apply" method
90+
buf := &bytes.Buffer{}
91+
if form.Revert {
92+
err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
93+
} else {
94+
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
95+
}
96+
if err == nil {
97+
opts.Content = buf.String()
98+
_, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
99+
}
100+
if err != nil {
101+
editorHandleFileOperationError(ctx, branchName, err)
102+
return
103+
}
104+
}
105+
106+
if form.CommitChoice == editorCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
107+
ctx.JSONRedirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
108+
} else {
109+
ctx.JSONRedirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
110+
}
111+
}

‎routers/web/repo/editor_error.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2025 Gitea Gogs Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"errors"
8+
9+
git_model "code.gitea.io/gitea/models/git"
10+
"code.gitea.io/gitea/modules/git"
11+
"code.gitea.io/gitea/modules/log"
12+
"code.gitea.io/gitea/modules/setting"
13+
"code.gitea.io/gitea/modules/util"
14+
"code.gitea.io/gitea/routers/utils"
15+
context_service "code.gitea.io/gitea/services/context"
16+
files_service "code.gitea.io/gitea/services/repository/files"
17+
)
18+
19+
func errorAs[T error](v error) (e T, ok bool) {
20+
if errors.As(v, &e) {
21+
return e, true
22+
}
23+
return e, false
24+
}
25+
26+
func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
27+
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
28+
"Message": message,
29+
"Summary": summary,
30+
"Details": utils.SanitizeFlashErrorString(details),
31+
})
32+
if err == nil {
33+
ctx.JSONError(flashError)
34+
} else {
35+
log.Error("RenderToHTML: %v", err)
36+
ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
37+
}
38+
}
39+
40+
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
41+
if git.IsErrNotExist(err) {
42+
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", ctx.Repo.TreePath))
43+
} else if git_model.IsErrLFSFileLocked(err) {
44+
ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName))
45+
} else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
46+
ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
47+
} else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](errAs); ok {
48+
switch errAs.Type {
49+
case git.EntryModeSymlink:
50+
ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
51+
case git.EntryModeTree:
52+
ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
53+
case git.EntryModeBlob:
54+
ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
55+
default:
56+
ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
57+
}
58+
} else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](errAs); ok {
59+
ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
60+
} else if errAs, ok := errorAs[git.ErrBranchNotExist](errAs); ok {
61+
ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
62+
} else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](errAs); ok {
63+
ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
64+
} else if files_service.IsErrCommitIDDoesNotMatch(errAs) {
65+
ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
66+
} else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
67+
ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
68+
} else if errAs, ok := errorAs[*git.ErrPushRejected](errAs); ok {
69+
if errAs.Message == "" {
70+
ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
71+
} else {
72+
editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
73+
}
74+
} else {
75+
setting.PanicInDevOrTesting("unclear err: %v", err)
76+
editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
77+
}
78+
}

‎routers/web/repo/editor_preview.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
9+
"code.gitea.io/gitea/modules/web"
10+
"code.gitea.io/gitea/services/context"
11+
"code.gitea.io/gitea/services/forms"
12+
files_service "code.gitea.io/gitea/services/repository/files"
13+
)
14+
15+
// DiffPreviewPost render preview diff page
16+
func DiffPreviewPost(ctx *context.Context) {
17+
form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
18+
treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
19+
if treePath == "" {
20+
ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
21+
return
22+
}
23+
24+
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
25+
if err != nil {
26+
ctx.ServerError("GetTreeEntryByPath", err)
27+
return
28+
} else if entry.IsDir() {
29+
ctx.HTTPError(http.StatusUnprocessableEntity)
30+
return
31+
}
32+
33+
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
34+
if err != nil {
35+
ctx.ServerError("GetDiffPreview", err)
36+
return
37+
}
38+
39+
if len(diff.Files) != 0 {
40+
ctx.Data["File"] = diff.Files[0]
41+
}
42+
43+
ctx.HTML(http.StatusOK, tplEditDiffPreview)
44+
}

‎routers/web/repo/editor_test.go

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,27 @@ package repo
66
import (
77
"testing"
88

9+
repo_model "code.gitea.io/gitea/models/repo"
910
"code.gitea.io/gitea/models/unittest"
1011
"code.gitea.io/gitea/modules/git"
1112
"code.gitea.io/gitea/modules/gitrepo"
12-
"code.gitea.io/gitea/services/contexttest"
1313

1414
"github.com/stretchr/testify/assert"
1515
)
1616

17-
func TestCleanUploadName(t *testing.T) {
17+
func TestEditorUtils(t *testing.T) {
1818
unittest.PrepareTestEnv(t)
19-
20-
kases := map[string]string{
21-
".git/refs/master": "",
22-
"/root/abc": "root/abc",
23-
"./../../abc": "abc",
24-
"a/../.git": "",
25-
"a/../../../abc": "abc",
26-
"../../../acd": "acd",
27-
"../../.git/abc": "",
28-
"..\\..\\.git/abc": "..\\..\\.git/abc",
29-
"..\\../.git/abc": "",
30-
"..\\../.git": "",
31-
"abc/../def": "def",
32-
".drone.yml": ".drone.yml",
33-
".abc/def/.drone.yml": ".abc/def/.drone.yml",
34-
"..drone.yml.": "..drone.yml.",
35-
"..a.dotty...name...": "..a.dotty...name...",
36-
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
37-
}
38-
for k, v := range kases {
39-
assert.Equal(t, cleanUploadFileName(k), v)
40-
}
41-
}
42-
43-
func TestGetUniquePatchBranchName(t *testing.T) {
44-
unittest.PrepareTestEnv(t)
45-
ctx, _ := contexttest.MockContext(t, "user2/repo1")
46-
ctx.SetPathParam("id", "1")
47-
contexttest.LoadRepo(t, ctx, 1)
48-
contexttest.LoadRepoCommit(t, ctx)
49-
contexttest.LoadUser(t, ctx, 2)
50-
contexttest.LoadGitRepo(t, ctx)
51-
defer ctx.Repo.GitRepo.Close()
52-
53-
expectedBranchName := "user2-patch-1"
54-
branchName := GetUniquePatchBranchName(ctx)
55-
assert.Equal(t, expectedBranchName, branchName)
56-
}
57-
58-
func TestGetClosestParentWithFiles(t *testing.T) {
59-
unittest.PrepareTestEnv(t)
60-
ctx, _ := contexttest.MockContext(t, "user2/repo1")
61-
ctx.SetPathParam("id", "1")
62-
contexttest.LoadRepo(t, ctx, 1)
63-
contexttest.LoadRepoCommit(t, ctx)
64-
contexttest.LoadUser(t, ctx, 2)
65-
contexttest.LoadGitRepo(t, ctx)
66-
defer ctx.Repo.GitRepo.Close()
67-
68-
repo := ctx.Repo.Repository
69-
branch := repo.DefaultBranch
70-
gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
71-
defer gitRepo.Close()
72-
commit, _ := gitRepo.GetBranchCommit(branch)
73-
var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
74-
for _, deletedFile := range []string{
75-
"dir1/dir2/dir3/file.txt",
76-
"file.txt",
77-
} {
78-
treePath := GetClosestParentWithFiles(deletedFile, commit)
79-
assert.Equal(t, expectedTreePath, treePath)
80-
}
19+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
20+
t.Run("getUniquePatchBranchName", func(t *testing.T) {
21+
branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
22+
assert.Equal(t, "user2-patch-1", branchName)
23+
})
24+
t.Run("getClosestParentWithFiles", func(t *testing.T) {
25+
gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
26+
defer gitRepo.Close()
27+
treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
28+
assert.Equal(t, "docs", treePath)
29+
treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
30+
assert.Empty(t, treePath)
31+
})
8132
}

‎routers/web/repo/editor_uploader.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"net/http"
8+
9+
repo_model "code.gitea.io/gitea/models/repo"
10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/util"
12+
"code.gitea.io/gitea/modules/web"
13+
"code.gitea.io/gitea/services/context"
14+
"code.gitea.io/gitea/services/context/upload"
15+
"code.gitea.io/gitea/services/forms"
16+
files_service "code.gitea.io/gitea/services/repository/files"
17+
)
18+
19+
// UploadFileToServer upload file to server file dir not git
20+
func UploadFileToServer(ctx *context.Context) {
21+
file, header, err := ctx.Req.FormFile("file")
22+
if err != nil {
23+
ctx.ServerError("FormFile", err)
24+
return
25+
}
26+
defer file.Close()
27+
28+
buf := make([]byte, 1024)
29+
n, _ := util.ReadAtMost(file, buf)
30+
if n > 0 {
31+
buf = buf[:n]
32+
}
33+
34+
err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
35+
if err != nil {
36+
ctx.HTTPError(http.StatusBadRequest, err.Error())
37+
return
38+
}
39+
40+
name := files_service.CleanGitTreePath(header.Filename)
41+
if len(name) == 0 {
42+
ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
43+
return
44+
}
45+
46+
uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
47+
if err != nil {
48+
ctx.ServerError("NewUpload", err)
49+
return
50+
}
51+
52+
ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
53+
}
54+
55+
// RemoveUploadFileFromServer remove file from server file dir
56+
func RemoveUploadFileFromServer(ctx *context.Context) {
57+
form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
58+
if form.File == "" {
59+
ctx.Status(http.StatusNoContent)
60+
return
61+
}
62+
if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
63+
ctx.ServerError("DeleteUploadByUUID", err)
64+
return
65+
}
66+
ctx.Status(http.StatusNoContent)
67+
}

‎routers/web/repo/editor_util.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"path"
10+
"strings"
11+
12+
git_model "code.gitea.io/gitea/models/git"
13+
repo_model "code.gitea.io/gitea/models/repo"
14+
"code.gitea.io/gitea/modules/git"
15+
"code.gitea.io/gitea/modules/json"
16+
"code.gitea.io/gitea/modules/log"
17+
"code.gitea.io/gitea/modules/util"
18+
context_service "code.gitea.io/gitea/services/context"
19+
)
20+
21+
// getUniquePatchBranchName Gets a unique branch name for a new patch branch
22+
// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
23+
// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
24+
// type in the branch name themselves (will be an empty field)
25+
func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
26+
prefix := prefixName + "-patch-"
27+
for i := 1; i <= 1000; i++ {
28+
branchName := fmt.Sprintf("%s%d", prefix, i)
29+
if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
30+
log.Error("getUniquePatchBranchName: %v", err)
31+
return ""
32+
} else if !exist {
33+
return branchName
34+
}
35+
}
36+
return ""
37+
}
38+
39+
// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
40+
// deleted. It returns "" for the tree root if no parents other than the root have files.
41+
func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
42+
var f func(treePath string, commit *git.Commit) string
43+
f = func(treePath string, commit *git.Commit) string {
44+
if treePath == "" || treePath == "." {
45+
return ""
46+
}
47+
// see if the tree has entries
48+
if tree, err := commit.SubTree(treePath); err != nil {
49+
return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
50+
} else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
51+
return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
52+
}
53+
return treePath
54+
}
55+
commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
56+
if err != nil {
57+
log.Error("GetBranchCommit: %v", err)
58+
return ""
59+
}
60+
return f(originTreePath, commit)
61+
}
62+
63+
// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
64+
func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
65+
ec, _, err := ctx.Repo.GetEditorconfig()
66+
if err == nil {
67+
def, err := ec.GetDefinitionForFilename(treePath)
68+
if err == nil {
69+
jsonStr, _ := json.Marshal(def)
70+
return string(jsonStr)
71+
}
72+
}
73+
return "null"
74+
}
75+
76+
// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
77+
// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
78+
// or: []{""}, []{""} for the root treePath
79+
func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
80+
treeNames = strings.Split(treePath, "/")
81+
treePaths = make([]string, len(treeNames))
82+
for i := range treeNames {
83+
treePaths[i] = strings.Join(treeNames[:i+1], "/")
84+
}
85+
return treeNames, treePaths
86+
}
87+
88+
func buildEditorCommitMessage(def, summary, body string) string {
89+
message := util.IfZero(strings.TrimSpace(summary), def)
90+
body = strings.TrimSpace(body)
91+
if body != "" {
92+
message += "\n\n" + body
93+
}
94+
return message
95+
}

‎routers/web/repo/patch.go

Lines changed: 0 additions & 126 deletions
This file was deleted.

‎services/context/repo.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,22 @@ func RepoMustNotBeArchived() func(ctx *Context) {
9494
}
9595
}
9696

97-
// CanCommitToBranchResults represents the results of CanCommitToBranch
98-
type CanCommitToBranchResults struct {
99-
CanCommitToBranch bool
100-
EditorEnabled bool
101-
UserCanPush bool
102-
RequireSigned bool
103-
WillSign bool
104-
SigningKey *git.SigningKey
105-
WontSignReason string
97+
type CommitFormBehaviors struct {
98+
CanCommitToBranch bool
99+
EditorEnabled bool
100+
UserCanPush bool
101+
RequireSigned bool
102+
WillSign bool
103+
SigningKey *git.SigningKey
104+
WontSignReason string
105+
CanCreatePullRequest bool
106+
CanCreateBasePullRequest bool
106107
}
107108

108-
// CanCommitToBranch returns true if repository is editable and user has proper access level
109-
//
110-
// and branch is not protected for push
111-
func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
109+
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
112110
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
113111
if err != nil {
114-
return CanCommitToBranchResults{}, err
112+
return nil, err
115113
}
116114
userCanPush := true
117115
requireSigned := false
@@ -138,14 +136,20 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use
138136
}
139137
}
140138

141-
return CanCommitToBranchResults{
139+
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
140+
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
141+
142+
return &CommitFormBehaviors{
142143
CanCommitToBranch: canCommit,
143144
EditorEnabled: canEnableEditor,
144145
UserCanPush: userCanPush,
145146
RequireSigned: requireSigned,
146147
WillSign: sign,
147148
SigningKey: keyID,
148149
WontSignReason: wontSignReason,
150+
151+
CanCreatePullRequest: canCreatePullRequest,
152+
CanCreateBasePullRequest: canCreateBasePullRequest,
149153
}, err
150154
}
151155

‎services/context/upload/upload.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
113113
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
114114
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
115115
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
116+
default:
117+
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
116118
}
117119
}

‎services/repository/files/content.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refComm
4242
}
4343

4444
// Check that the path given in opts.treePath is valid (not a git path)
45-
cleanTreePath := CleanUploadFileName(treePath)
45+
cleanTreePath := CleanGitTreePath(treePath)
4646
if cleanTreePath == "" && treePath != "" {
4747
return nil, ErrFilenameInvalid{
4848
Path: treePath,
@@ -103,7 +103,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
103103
// GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
104104
func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) {
105105
// Check that the path given in opts.treePath is valid (not a git path)
106-
cleanTreePath := CleanUploadFileName(treePath)
106+
cleanTreePath := CleanGitTreePath(treePath)
107107
if cleanTreePath == "" && treePath != "" {
108108
return nil, ErrFilenameInvalid{
109109
Path: treePath,

‎services/repository/files/file.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error {
134134
return util.ErrInvalidArgument
135135
}
136136

137-
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
138-
func CleanUploadFileName(name string) string {
137+
// CleanGitTreePath Trims a filename and returns empty string if it is a .git directory
138+
func CleanGitTreePath(name string) string {
139139
// Rebase the filename
140140
name = util.PathJoinRel(name)
141141
// Git disallows any filenames to have a .git directory in them.
@@ -144,5 +144,8 @@ func CleanUploadFileName(name string) string {
144144
return ""
145145
}
146146
}
147+
if name == "." {
148+
name = ""
149+
}
147150
return name
148151
}

‎services/repository/files/file_test.go

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,9 @@ import (
1010
)
1111

1212
func TestCleanUploadFileName(t *testing.T) {
13-
t.Run("Clean regular file", func(t *testing.T) {
14-
name := "this/is/test"
15-
cleanName := CleanUploadFileName(name)
16-
expectedCleanName := name
17-
assert.Equal(t, expectedCleanName, cleanName)
18-
})
19-
20-
t.Run("Clean a .git path", func(t *testing.T) {
21-
name := "this/is/test/.git"
22-
cleanName := CleanUploadFileName(name)
23-
expectedCleanName := ""
24-
assert.Equal(t, expectedCleanName, cleanName)
25-
})
13+
assert.Equal(t, "", CleanGitTreePath("")) //nolint
14+
assert.Equal(t, "", CleanGitTreePath(".")) //nolint
15+
assert.Equal(t, "a/b", CleanGitTreePath("a/b"))
16+
assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint
17+
assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint
2618
}

‎services/repository/files/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
127127
}
128128

129129
// Check that the path given in opts.treePath is valid (not a git path)
130-
treePath := CleanUploadFileName(file.TreePath)
130+
treePath := CleanGitTreePath(file.TreePath)
131131
if treePath == "" {
132132
return nil, ErrFilenameInvalid{
133133
Path: file.TreePath,
134134
}
135135
}
136136
// If there is a fromTreePath (we are copying it), also clean it up
137-
fromTreePath := CleanUploadFileName(file.FromTreePath)
137+
fromTreePath := CleanGitTreePath(file.FromTreePath)
138138
if fromTreePath == "" && file.FromTreePath != "" {
139139
return nil, ErrFilenameInvalid{
140140
Path: file.FromTreePath,

‎services/repository/files/upload.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
180180
return err
181181
}
182182

183+
if repo.IsEmpty {
184+
_ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty")
185+
}
186+
183187
return repo_model.DeleteUploads(ctx, uploads...)
184188
}
185189

‎templates/repo/editor/cherry_pick.tmpl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
{{template "repo/header" .}}
44
<div class="ui container">
55
{{template "base/alert" .}}
6-
<form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}">
6+
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
77
{{.CsrfTokenHtml}}
88
<input type="hidden" name="last_commit" value="{{.last_commit}}">
9-
<input type="hidden" name="page_has_posted" value="true">
109
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
1110
<div class="repo-editor-header">
12-
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
13-
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
14-
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}}
11+
<div class="ui breadcrumb field">
12+
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}}
13+
{{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}}
1514
{{if eq .CherryPickType "revert"}}
1615
{{ctx.Locale.Tr "repo.editor.revert" $shalink}}
1716
{{else}}

‎templates/repo/editor/commit_form.tmpl

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div class="commit-form-wrapper">
22
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
33
<div class="commit-form">
4-
<h3>{{- if .CanCommitToBranch.WillSign}}
5-
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span>
4+
<h3>{{- if .CommitFormBehaviors.WillSign}}
5+
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
66
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
77
{{- else}}
8-
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
8+
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
99
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
1010
{{- end}}</h3>
1111
<div class="field">
@@ -22,17 +22,17 @@
2222
</div>
2323
<div class="quick-pull-choice js-quick-pull-choice">
2424
<div class="field">
25-
<div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}">
25+
<div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
2626
<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}}>
2727
<label>
2828
{{svg "octicon-git-commit"}}
2929
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
30-
{{if not .CanCommitToBranch.CanCommitToBranch}}
30+
{{if not .CommitFormBehaviors.CanCommitToBranch}}
3131
<div class="ui visible small warning message">
3232
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
3333
<ul>
34-
{{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
35-
{{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
34+
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
35+
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
3636
</ul>
3737
</div>
3838
{{end}}
@@ -42,14 +42,14 @@
4242
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
4343
<div class="field">
4444
<div class="ui radio checkbox">
45-
{{if .CanCreatePullRequest}}
45+
{{if .CommitFormBehaviors.CanCreatePullRequest}}
4646
<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}}>
4747
{{else}}
4848
<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}}>
4949
{{end}}
5050
<label>
5151
{{svg "octicon-git-pull-request"}}
52-
{{if .CanCreatePullRequest}}
52+
{{if .CommitFormBehaviors.CanCreatePullRequest}}
5353
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
5454
{{else}}
5555
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
@@ -58,7 +58,7 @@
5858
</div>
5959
</div>
6060
<div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}">
61-
<div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}">
61+
<div class="new-branch-name-input field">
6262
{{svg "octicon-git-branch"}}
6363
<input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}">
6464
<span class="text-muted js-quick-pull-normalization-info"></span>
@@ -67,7 +67,7 @@
6767
{{end}}
6868
</div>
6969
{{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}}
70-
<div class="field {{if .Err_CommitEmail}}error{{end}}">
70+
<div class="field">
7171
<label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label>
7272
<select class="ui selection dropdown" name="commit_email">
7373
{{- range $email := .CommitCandidateEmails -}}
@@ -77,7 +77,7 @@
7777
</div>
7878
{{end}}
7979
</div>
80-
<button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}>
80+
<button id="commit-button" type="submit" class="ui primary button">
8181
{{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}}
8282
</button>
8383
<a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a>

‎templates/repo/editor/delete.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{{template "repo/header" .}}
44
<div class="ui container">
55
{{template "base/alert" .}}
6-
<form class="ui form" method="post">
6+
<form class="ui form form-fetch-action" method="post">
77
{{.CsrfTokenHtml}}
88
<input type="hidden" name="last_commit" value="{{.last_commit}}">
99
{{template "repo/editor/commit_form" .}}

‎templates/repo/editor/edit.tmpl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
{{template "repo/header" .}}
44
<div class="ui container">
55
{{template "base/alert" .}}
6-
<form class="ui edit form" method="post"
6+
<form class="ui edit form form-fetch-action" method="post"
77
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
88
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
99
>
1010
{{.CsrfTokenHtml}}
1111
<input type="hidden" name="last_commit" value="{{.last_commit}}">
12-
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
1312
<div class="repo-editor-header">
14-
<div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}">
13+
<div class="ui breadcrumb field">
1514
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
1615
{{$n := len .TreeNames}}
1716
{{$l := Eval $n "-" 1}}

‎templates/repo/editor/patch.tmpl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
{{template "repo/header" .}}
44
<div class="ui container">
55
{{template "base/alert" .}}
6-
<form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
6+
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
77
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
88
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
99
>
1010
{{.CsrfTokenHtml}}
1111
<input type="hidden" name="last_commit" value="{{.last_commit}}">
12-
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
1312
<div class="repo-editor-header">
14-
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
13+
<div class="ui breadcrumb field">
1514
{{ctx.Locale.Tr "repo.editor.patching"}}
1615
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
1716
<div class="breadcrumb-divider">:</div>

‎templates/repo/editor/upload.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
{{template "repo/header" .}}
44
<div class="ui container">
55
{{template "base/alert" .}}
6-
<form class="ui comment form" method="post">
6+
<form class="ui comment form form-fetch-action" method="post">
77
{{.CsrfTokenHtml}}
88
<div class="repo-editor-header">
9-
<div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}">
9+
<div class="ui breadcrumb field">
1010
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
1111
{{$n := len .TreeNames}}
1212
{{$l := Eval $n "-" 1}}

‎tests/integration/api_repo_languages_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"testing"
1010
"time"
1111

12+
"code.gitea.io/gitea/modules/test"
13+
1214
"github.com/stretchr/testify/assert"
1315
)
1416

@@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) {
3234
"content": "package main",
3335
"commit_choice": "direct",
3436
})
35-
session.MakeRequest(t, req, http.StatusSeeOther)
37+
resp = session.MakeRequest(t, req, http.StatusOK)
38+
assert.NotEmpty(t, test.RedirectURL(resp))
3639

3740
// let gitea calculate language stats
3841
time.Sleep(time.Second)

‎tests/integration/api_repo_license_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import (
1212

1313
auth_model "code.gitea.io/gitea/models/auth"
1414
api "code.gitea.io/gitea/modules/structs"
15+
"code.gitea.io/gitea/modules/test"
1516

1617
"github.com/stretchr/testify/assert"
1718
)
1819

1920
var testLicenseContent = `
20-
Copyright (c) 2024 Gitea
21+
Copyright (c) 2024 Gitea
2122
2223
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
2324
@@ -48,7 +49,8 @@ func TestAPIRepoLicense(t *testing.T) {
4849
"content": testLicenseContent,
4950
"commit_choice": "direct",
5051
})
51-
session.MakeRequest(t, req, http.StatusSeeOther)
52+
resp = session.MakeRequest(t, req, http.StatusOK)
53+
assert.NotEmpty(t, test.RedirectURL(resp))
5254

5355
// let gitea update repo license
5456
time.Sleep(time.Second)

‎tests/integration/editor_test.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
user_model "code.gitea.io/gitea/models/user"
2121
"code.gitea.io/gitea/modules/git"
2222
"code.gitea.io/gitea/modules/json"
23+
"code.gitea.io/gitea/modules/test"
2324
"code.gitea.io/gitea/modules/translation"
2425
"code.gitea.io/gitea/tests"
2526

@@ -34,7 +35,7 @@ func TestCreateFile(t *testing.T) {
3435
})
3536
}
3637

37-
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder {
38+
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
3839
// Request editor page
3940
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
4041
req := NewRequest(t, "GET", newURL)
@@ -52,7 +53,8 @@ func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, file
5253
"content": content,
5354
"commit_choice": "direct",
5455
})
55-
return session.MakeRequest(t, req, http.StatusSeeOther)
56+
resp = session.MakeRequest(t, req, http.StatusOK)
57+
assert.NotEmpty(t, test.RedirectURL(resp))
5658
}
5759

5860
func TestCreateFileOnProtectedBranch(t *testing.T) {
@@ -88,9 +90,9 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
8890
"commit_choice": "direct",
8991
})
9092

91-
resp = session.MakeRequest(t, req, http.StatusOK)
92-
// Check body for error message
93-
assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch &#34;master&#34;.")
93+
resp = session.MakeRequest(t, req, http.StatusBadRequest)
94+
respErr := test.ParseJSONError(resp.Body.Bytes())
95+
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
9496

9597
// remove the protected branch
9698
csrf = GetUserCSRFToken(t, session)
@@ -131,7 +133,8 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
131133
"commit_choice": "direct",
132134
},
133135
)
134-
session.MakeRequest(t, req, http.StatusSeeOther)
136+
resp = session.MakeRequest(t, req, http.StatusOK)
137+
assert.NotEmpty(t, test.RedirectURL(resp))
135138

136139
// Verify the change
137140
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
@@ -161,7 +164,8 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
161164
"new_branch_name": targetBranch,
162165
},
163166
)
164-
session.MakeRequest(t, req, http.StatusSeeOther)
167+
resp = session.MakeRequest(t, req, http.StatusOK)
168+
assert.NotEmpty(t, test.RedirectURL(resp))
165169

166170
// Verify the change
167171
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
@@ -211,9 +215,8 @@ func TestWebGitCommitEmail(t *testing.T) {
211215
newCommit := getLastCommit(t)
212216
if expectedUserName == "" {
213217
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
214-
htmlDoc := NewHTMLParser(t, resp.Body)
215-
errMsg := htmlDoc.doc.Find(".ui.negative.message").Text()
216-
assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email"))
218+
respErr := test.ParseJSONError(resp.Body.Bytes())
219+
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
217220
} else {
218221
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
219222
assert.Equal(t, expectedUserName, newCommit.Author.Name)
@@ -333,7 +336,7 @@ index 0000000000..bbbbbbbbbb
333336
)
334337

335338
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch
336-
assert.Equal(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location"))
339+
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
337340
})
338341
})
339342
}

‎tests/integration/empty_repo_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"io"
1111
"mime/multipart"
1212
"net/http"
13-
"net/http/httptest"
1413
"strings"
1514
"testing"
1615

@@ -30,15 +29,16 @@ import (
3029
"github.com/stretchr/testify/require"
3130
)
3231

33-
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
32+
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) {
3433
url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
3534
req := NewRequestWithValues(t, "POST", url, map[string]string{
3635
"_csrf": GetUserCSRFToken(t, session),
3736
"commit_choice": "direct",
3837
"tree_path": treePath,
3938
"content": content,
4039
})
41-
return session.MakeRequest(t, req, http.StatusSeeOther)
40+
resp := session.MakeRequest(t, req, http.StatusOK)
41+
assert.NotEmpty(t, test.RedirectURL(resp))
4242
}
4343

4444
func TestEmptyRepo(t *testing.T) {
@@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) {
8787
"content": "newly-added-test-file",
8888
})
8989

90-
resp = session.MakeRequest(t, req, http.StatusSeeOther)
90+
resp = session.MakeRequest(t, req, http.StatusOK)
9191
redirect := test.RedirectURL(resp)
9292
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect)
9393

@@ -154,7 +154,7 @@ func TestEmptyRepoUploadFile(t *testing.T) {
154154
"files": respMap["uuid"],
155155
"tree_path": "",
156156
})
157-
resp = session.MakeRequest(t, req, http.StatusSeeOther)
157+
resp = session.MakeRequest(t, req, http.StatusOK)
158158
redirect := test.RedirectURL(resp)
159159
assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect)
160160

‎tests/integration/pull_compare_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) {
159159
"commit_summary": "user2 updated the file",
160160
"commit_choice": "direct",
161161
})
162-
user2Session.MakeRequest(t, req, http.StatusSeeOther)
162+
resp = user2Session.MakeRequest(t, req, http.StatusOK)
163+
assert.NotEmpty(t, test.RedirectURL(resp))
163164
}
164165
}
165166
})

‎web_src/js/features/common-fetch-action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
7070
}
7171

7272
const formMethod = formEl.getAttribute('method') || 'get';
73-
const formActionUrl = formEl.getAttribute('action');
73+
const formActionUrl = formEl.getAttribute('action') || window.location.href;
7474
const formData = new FormData(formEl);
7575
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
7676
if (submitterName) {

‎web_src/js/features/repo-editor.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts';
77
import {confirmModal} from './comp/ConfirmModal.ts';
88
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
99
import {fomanticQuery} from '../modules/fomantic/base.ts';
10+
import {submitFormFetchAction} from './common-fetch-action.ts';
1011

1112
function initEditPreviewTab(elForm: HTMLFormElement) {
1213
const elTabMenu = elForm.querySelector('.repo-editor-menu');
@@ -143,31 +144,28 @@ export function initRepoEditor() {
143144

144145
const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
145146

147+
// on the upload page, there is no editor(textarea)
148+
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
149+
if (!editArea) return;
150+
146151
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
147152
// to enable or disable the commit button
148153
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
149154
const dirtyFileClass = 'dirty-file';
150155

151-
// Enabling the button at the start if the page has posted
152-
if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') {
153-
commitButton.disabled = false;
154-
}
155-
156+
const syncCommitButtonState = () => {
157+
const dirty = elForm.classList.contains(dirtyFileClass);
158+
commitButton.disabled = !dirty;
159+
};
156160
// Registering a custom listener for the file path and the file content
157161
// FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
158162
applyAreYouSure(elForm, {
159163
silent: true,
160164
dirtyClass: dirtyFileClass,
161165
fieldSelector: ':input:not(.commit-form-wrapper :input)',
162-
change($form: any) {
163-
const dirty = $form[0]?.classList.contains(dirtyFileClass);
164-
commitButton.disabled = !dirty;
165-
},
166+
change: syncCommitButtonState,
166167
});
167-
168-
// on the upload page, there is no editor(textarea)
169-
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
170-
if (!editArea) return;
168+
syncCommitButtonState(); // disable the "commit" button when no content changes
171169

172170
initEditPreviewTab(elForm);
173171

@@ -182,7 +180,7 @@ export function initRepoEditor() {
182180
editor.setValue(value);
183181
}
184182

185-
commitButton?.addEventListener('click', async (e) => {
183+
commitButton.addEventListener('click', async (e) => {
186184
// A modal which asks if an empty file should be committed
187185
if (!editArea.value) {
188186
e.preventDefault();
@@ -191,7 +189,7 @@ export function initRepoEditor() {
191189
content: elForm.getAttribute('data-text-empty-confirm-content'),
192190
})) {
193191
ignoreAreYouSure(elForm);
194-
elForm.submit();
192+
submitFormFetchAction(elForm);
195193
}
196194
}
197195
});

0 commit comments

Comments
 (0)
Please sign in to comment.