diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go
index ae0fbf0abd..d1c587671b 100644
--- a/models/renderhelper/repo_comment.go
+++ b/models/renderhelper/repo_comment.go
@@ -34,7 +34,7 @@ func (r *RepoComment) ResolveLink(link, preferLinkType string) string {
case markup.LinkTypeRoot:
return r.ctx.ResolveLinkRoot(link)
default:
- return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
+ return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefSubURL, link)
}
}
@@ -43,7 +43,7 @@ var _ markup.RenderHelper = (*RepoComment)(nil)
type RepoCommentOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
- CurrentRefPath string // eg: "branch/main" or "commit/11223344"
+ CurrentRefSubURL string // eg: "branch/main" or "commit/11223344"
FootnoteContextID string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page
}
diff --git a/models/renderhelper/repo_comment_test.go b/models/renderhelper/repo_comment_test.go
index 3b13bff73c..1443f8b3c0 100644
--- a/models/renderhelper/repo_comment_test.go
+++ b/models/renderhelper/repo_comment_test.go
@@ -54,8 +54,8 @@ func TestRepoComment(t *testing.T) {
`, rendered)
})
- t.Run("WithCurrentRefPath", func(t *testing.T) {
- rctx := NewRenderContextRepoComment(t.Context(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}).
+ t.Run("WithCurrentRefSubURL", func(t *testing.T) {
+ rctx := NewRenderContextRepoComment(t.Context(), repo1, RepoCommentOptions{CurrentRefSubURL: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
// the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path
diff --git a/models/renderhelper/repo_file.go b/models/renderhelper/repo_file.go
index 5d0bfd6c80..d9aa71b727 100644
--- a/models/renderhelper/repo_file.go
+++ b/models/renderhelper/repo_file.go
@@ -35,11 +35,11 @@ func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) {
case markup.LinkTypeRoot:
finalLink = r.ctx.ResolveLinkRoot(link)
case markup.LinkTypeRaw:
- finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
+ finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link)
case markup.LinkTypeMedia:
- finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
+ finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link)
default:
- finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
+ finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link)
}
return finalLink
}
@@ -50,8 +50,8 @@ type RepoFileOptions struct {
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
- CurrentRefPath string // eg: "branch/main", it is a sub URL path escaped by callers, TODO: rename to CurrentRefSubURL
- CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping
+ CurrentRefSubURL string // eg: "branch/main" or "branch/my%20branch", it is a sub URL path escaped by callers
+ CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping
}
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
@@ -71,9 +71,8 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
})
}
// External render's iframe needs this to generate correct links
- // TODO: maybe need to make it access "CurrentRefPath" directly (but impossible at the moment due to cycle-import)
- // CurrentRefPath is already path-escaped by callers
- rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefPath
+ // TODO: maybe need to make it access "CurrentRefSubURL" directly (but impossible at the moment due to cycle-import)
+ rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefSubURL
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
return rctx
}
diff --git a/models/renderhelper/repo_file_test.go b/models/renderhelper/repo_file_test.go
index 3b48efba3a..72d98efc66 100644
--- a/models/renderhelper/repo_file_test.go
+++ b/models/renderhelper/repo_file_test.go
@@ -36,7 +36,7 @@ func TestRepoFile(t *testing.T) {
})
t.Run("AbsoluteAndRelative", func(t *testing.T) {
- rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}).
+ rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "branch/main"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
@@ -53,8 +53,8 @@ func TestRepoFile(t *testing.T) {
`, rendered)
})
- t.Run("WithCurrentRefPath", func(t *testing.T) {
- rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}).
+ t.Run("WithCurrentRefSubURL", func(t *testing.T) {
+ rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "/commit/1234"}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
[/test](/test)
@@ -66,10 +66,10 @@ func TestRepoFile(t *testing.T) {
`, rendered)
})
- t.Run("WithCurrentRefPathByTag", func(t *testing.T) {
+ t.Run("WithCurrentRefSubURLByTag", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{
- CurrentRefPath: "/commit/1234",
- CurrentTreePath: "my-dir",
+ CurrentRefSubURL: "/commit/1234",
+ CurrentTreePath: "my-dir",
}).
WithMarkupType(markdown.MarkupName)
rendered, err := markup.RenderString(rctx, `
@@ -89,8 +89,8 @@ func TestRepoFileOrgMode(t *testing.T) {
t.Run("Links", func(t *testing.T) {
rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{
- CurrentRefPath: "/commit/1234",
- CurrentTreePath: "my-dir",
+ CurrentRefSubURL: "/commit/1234",
+ CurrentTreePath: "my-dir",
}).WithRelativePath("my-dir/a.org")
rendered, err := markup.RenderString(rctx, `
diff --git a/models/renderhelper/repo_wiki.go b/models/renderhelper/repo_wiki.go
index 218b1e4a67..61e2b570e5 100644
--- a/models/renderhelper/repo_wiki.go
+++ b/models/renderhelper/repo_wiki.go
@@ -36,9 +36,9 @@ func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) {
case markup.LinkTypeRoot:
finalLink = r.ctx.ResolveLinkRoot(link)
case markup.LinkTypeMedia, markup.LinkTypeRaw:
- finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
+ finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefSubURL), r.opts.currentTreePath, link)
default:
- finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
+ finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefSubURL), r.opts.currentTreePath, link)
}
return finalLink
}
@@ -50,8 +50,8 @@ type RepoWikiOptions struct {
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
// these options are not used at the moment because Wiki doesn't support sub-path, nor branch
- currentRefPath string // eg: "branch/main"
- currentTreePath string // eg: "path/to/file" in the repo
+ currentRefSubURL string // eg: "branch/main"
+ currentTreePath string // eg: "path/to/file" in the repo
}
func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, opts ...RepoWikiOptions) *markup.RenderContext {
diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go
index 8abf6f1887..6c2fe9b0d6 100644
--- a/modules/httplib/serve.go
+++ b/modules/httplib/serve.go
@@ -37,6 +37,42 @@ type ServeHeaderOptions struct {
LastModified time.Time
}
+const (
+ // Disable JS execution on the same origin, since we serve the file from the same origin as Gitea server.
+ // This rule can be relaxed in the future as long as it is properly sandboxed.
+ // "style-src" is for SVG inline styles (from Display SVG files as images instead of text #14101)
+ serveHeaderCspDefault = "default-src 'none'; style-src 'unsafe-inline'; sandbox"
+
+ // No sandbox attribute for PDF as it breaks rendering in at least Safari.
+ // This should generally be safe as scripts inside PDF can not escape the PDF document.
+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion.
+ // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
+ serveHeaderCspPdf = "default-src 'none'; style-src 'unsafe-inline'"
+
+ // For audios and videos, actually it doesn't really need CSP (just like Gitea <= 1.25)
+ serveHeaderCspAudioVideo = ""
+)
+
+func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) {
+ header := w.Header()
+ contentType = util.IfZero(contentType, typesniffer.MimeTypeApplicationOctetStream)
+ header.Set("Content-Type", contentType)
+ header.Set("X-Content-Type-Options", "nosniff")
+
+ csp := serveHeaderCspDefault
+ if strings.HasPrefix(contentType, "application/pdf") {
+ csp = serveHeaderCspPdf
+ }
+ if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") {
+ csp = serveHeaderCspAudioVideo
+ }
+ if csp != "" {
+ header.Set("Content-Security-Policy", csp)
+ } else {
+ header.Del("Content-Security-Policy")
+ }
+}
+
// ServeSetHeaders sets necessary content serve headers
func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
header := w.Header()
@@ -46,24 +82,11 @@ func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
w.Header().Add(gzhttp.HeaderNoCompression, "1")
}
- contentType := util.IfZero(opts.ContentType, typesniffer.MimeTypeApplicationOctetStream)
- header.Set("Content-Type", contentType)
- header.Set("X-Content-Type-Options", "nosniff")
+ serveSetHeaderContentRelated(w, opts.ContentType)
if opts.ContentLength != nil {
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
}
-
- // Disable script execution of HTML/SVG files, since we serve the file from the same origin as Gitea server
- header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
- if strings.Contains(contentType, "application/pdf") {
- // no sandbox attribute for PDF as it breaks rendering in at least safari. this
- // should generally be safe as scripts inside PDF can not escape the PDF document
- // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
- // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
- header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
- }
-
if opts.Filename != "" && opts.ContentDisposition != "" {
header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename)))
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go
index 38cf4c197f..2a245300b0 100644
--- a/modules/httplib/serve_test.go
+++ b/modules/httplib/serve_test.go
@@ -12,6 +12,8 @@ import (
"strings"
"testing"
+ "code.gitea.io/gitea/modules/typesniffer"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -106,3 +108,28 @@ func TestServeUserContentByFile(t *testing.T) {
test(t, http.StatusPartialContent, data[1:])
})
}
+
+func TestServeSetHeaderContentRelated(t *testing.T) {
+ cases := []struct {
+ contentType string
+ csp string
+ }{
+ {"", serveHeaderCspDefault},
+ {"any", serveHeaderCspDefault},
+ {"application/pdf", serveHeaderCspPdf},
+ {"application/pdf; other", serveHeaderCspPdf},
+ {"audio/mp4", serveHeaderCspAudioVideo},
+ {"video/ogg; other", serveHeaderCspAudioVideo},
+ {typesniffer.MimeTypeImageSvg, serveHeaderCspDefault},
+ }
+ for _, c := range cases {
+ w := httptest.NewRecorder()
+ serveSetHeaderContentRelated(w, c.contentType)
+ csp := w.Header().Get("Content-Security-Policy")
+ assert.Equal(t, c.csp, csp, "content-type: %s", c.contentType)
+ assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) // it should always be there
+ }
+
+ // make sure sandboxed
+ require.Contains(t, serveHeaderCspDefault, "; sandbox")
+}
diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json
index 0bab3a0010..5960e554b7 100644
--- a/options/locale/locale_zh-CN.json
+++ b/options/locale/locale_zh-CN.json
@@ -2474,7 +2474,7 @@
"repo.settings.tags.protection.allowed.noone": "无",
"repo.settings.tags.protection.create": "保护 Git 标签",
"repo.settings.tags.protection.none": "没有受保护的 Git 标签。",
- "repo.settings.tags.protection.pattern.description": "您可以使用单个名称或 glob 表达式匹配或正则表达式来匹配多个 Git 标签。了解详情请访问 保护标签指南。",
+ "repo.settings.tags.protection.pattern.description": "您可以使用单个名称或 glob 表达式匹配或正则表达式来匹配多个 Git 标签。欲了解详情请访问 保护标签指南。",
"repo.settings.bot_token": "Bot 令牌",
"repo.settings.chat_id": "聊天 ID",
"repo.settings.thread_id": "线程 ID",
@@ -3099,11 +3099,11 @@
"admin.packages.size": "大小",
"admin.packages.published": "已发布",
"admin.defaulthooks": "默认 Web 钩子",
- "admin.defaulthooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。详情请访问 Web 钩子指南。",
+ "admin.defaulthooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。欲了解详情请访问 Web 钩子指南。",
"admin.defaulthooks.add_webhook": "添加默认 Web 钩子",
"admin.defaulthooks.update_webhook": "更新默认 Web 钩子",
"admin.systemhooks": "系统 Web 钩子",
- "admin.systemhooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 Web 钩子指南。",
+ "admin.systemhooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。欲了解详情请访问 Web 钩子指南。",
"admin.systemhooks.add_webhook": "添加系统 Web 钩子",
"admin.systemhooks.update_webhook": "更新系统 Web 钩子",
"admin.auths.auth_manage_panel": "认证源管理",
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 35b1b21f6a..05f48c7902 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -71,7 +71,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
case "gfm": // legacy mode
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
- CurrentRefPath: refPath, CurrentTreePath: treePath,
+ CurrentRefSubURL: refPath, CurrentTreePath: treePath,
})
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "comment":
@@ -87,7 +87,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
case "file":
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
- CurrentRefPath: refPath, CurrentTreePath: treePath,
+ CurrentRefSubURL: refPath, CurrentTreePath: treePath,
})
rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension
default:
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 262b001e6a..56475c47f0 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -182,7 +182,7 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
}
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{
- CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)),
+ CurrentRefSubURL: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)),
})
ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes)
if err != nil {
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 10803c9fbf..7312478299 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -609,6 +609,8 @@ func DeleteTeam(ctx *context.Context) {
// TeamInvite renders the team invite page
func TeamInvite(ctx *context.Context) {
invite, org, team, inviter, err := getTeamInviteFromContext(ctx)
+ // TODO: to quickly debug the UI, can uncomment this (don't worry, it won't pass CI lint)
+ // invite, org, team, inviter, err = &org_model.TeamInvite{}, &org_model.Organization{}, &org_model.Team{}, ctx.Doer, nil
if err != nil {
if org_model.IsErrTeamInviteNotFound(err) {
ctx.NotFound(err)
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 736a2dff00..c118f41381 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -9,7 +9,6 @@ import (
"fmt"
"html/template"
"net/http"
- "path"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -409,7 +408,7 @@ func Diff(ctx *context.Context) {
if err == nil {
ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))})
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)})
htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage)
if err != nil {
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index af13a1156e..852b880ab0 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
- pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
@@ -826,6 +825,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
panic("impossible, issue must be the same")
}
+ pull := issue.PullRequest
data := &pullMergeBoxData{}
prInfo.MergeBoxData = data
@@ -834,14 +834,12 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
statusCheckData = &pullCommitStatusCheckData{} // make the following logic easier, no need to keep checking "nil"
}
- pull := issue.PullRequest
canDelete := false
allowMerge := false
canWriteToHeadRepo := false
pull_service.StartPullRequestCheckOnView(ctx, pull)
- ctx.Data["GetCommitMessages"] = ""
if !prInfo.IsPullRequestBroken {
var err error
ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
@@ -849,7 +847,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
ctx.ServerError("IsUserAllowedToUpdate", err)
return
}
- ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull)
}
if pull.IsFilesConflicted() {
@@ -903,59 +900,11 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
}
}
- data.ReloadingInterval = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
- ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
- ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
+ data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0)
+ data.ShowMergeInstructions = canWriteToHeadRepo
+ data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed
ctx.Data["AllowMerge"] = allowMerge
- prUnit, err := issue.Repo.GetUnit(ctx, unit.TypePullRequests)
- if err != nil {
- ctx.ServerError("GetUnit", err)
- return
- }
- prConfig := prUnit.PullRequestsConfig()
-
- ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge
-
- var mergeStyle repo_model.MergeStyle
- // Check correct values and select default
- if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
- !prConfig.IsMergeStyleAllowed(ms) {
- if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) && !ok {
- mergeStyle = prConfig.DefaultMergeStyle
- } else if prConfig.AllowMerge {
- mergeStyle = repo_model.MergeStyleMerge
- } else if prConfig.AllowRebase {
- mergeStyle = repo_model.MergeStyleRebase
- } else if prConfig.AllowRebaseMerge {
- mergeStyle = repo_model.MergeStyleRebaseMerge
- } else if prConfig.AllowSquash {
- mergeStyle = repo_model.MergeStyleSquash
- } else if prConfig.AllowFastForwardOnly {
- mergeStyle = repo_model.MergeStyleFastForwardOnly
- } else if prConfig.AllowManualMerge {
- mergeStyle = repo_model.MergeStyleManuallyMerged
- }
- }
-
- ctx.Data["MergeStyle"] = mergeStyle
-
- defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
- if err != nil {
- ctx.ServerError("GetDefaultMergeMessage", err)
- return
- }
- ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
- ctx.Data["DefaultMergeBody"] = defaultMergeBody
-
- defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
- if err != nil {
- ctx.ServerError("GetDefaultSquashMergeMessage", err)
- return
- }
- ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
- ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
-
pb := prInfo.ProtectedBranchRule
if pb != nil {
pb.Repo = pull.BaseRepo
@@ -995,6 +944,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
return
}
+ prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
+ data.AutodetectManualMerge = prConfig.AutodetectManualMerge
+
stillCanManualMerge := func() bool {
if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
return false
@@ -1007,13 +959,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
- // Check if there is a pending pr merge
- ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
- if err != nil {
- ctx.ServerError("GetScheduledMergeByPullID", err)
- return
- }
-
enableStatusCheck := pb != nil && pb.EnableStatusCheck
ctx.Data["EnableStatusCheck"] = enableStatusCheck
@@ -1043,6 +988,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
(!data.requireSigned || data.willSign) // signing requirement is satisfied
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
+ prInfo.prepareMergeBoxFormProps(ctx)
}
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index c532cbba22..7b31a26d6f 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -164,12 +164,13 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
func (prInfo *pullRequestViewInfo) setTemplateDataMergeTarget(ctx *context.Context) {
pull := prInfo.issue.PullRequest
if ctx.Repo.Owner.Name == pull.MustHeadUserName(ctx) {
- ctx.Data["HeadTarget"] = pull.HeadBranch
+ prInfo.headTarget = pull.HeadBranch
} else if pull.HeadRepo == nil {
- ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch)
+ prInfo.headTarget = ctx.Locale.TrString("repo.pull.deleted_branch", pull.HeadBranch)
} else {
- ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
+ prInfo.headTarget = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
}
+ ctx.Data["HeadTarget"] = prInfo.headTarget
ctx.Data["BaseTarget"] = pull.BaseBranch
headBranchLink := ""
if pull.Flow == issues_model.PullRequestFlowGithub {
@@ -268,6 +269,11 @@ type pullMergeBoxData struct {
HasOverridableBlockers bool
CanMergeNow bool
+ MergeFormProps map[string]any
+ ShowPullCommands bool
+ ShowMergeInstructions bool
+ AutodetectManualMerge bool
+
// don't expose unneeded fields to templates, need more refactoring changes
hasStatusCheckBlocker bool
isPullBranchDeletable bool
@@ -289,6 +295,7 @@ type pullRequestViewInfo struct {
IsPullRequestBroken bool
HeadBranchCommitID string
+ headTarget string // for display purpose only
CompareInfo git_service.CompareInfo
ProtectedBranchRule *git_model.ProtectedBranch
diff --git a/routers/web/repo/pull_merge_form.go b/routers/web/repo/pull_merge_form.go
new file mode 100644
index 0000000000..b390fd6934
--- /dev/null
+++ b/routers/web/repo/pull_merge_form.go
@@ -0,0 +1,147 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "html/template"
+
+ pull_model "code.gitea.io/gitea/models/pull"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ pull_service "code.gitea.io/gitea/services/pull"
+)
+
+func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) {
+ pull := prInfo.issue.PullRequest
+ prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
+
+ // Check correct values and select default
+ var mergeStyle repo_model.MergeStyle
+ if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) {
+ mergeStyle = prConfig.DefaultMergeStyle
+ } else if prConfig.AllowMerge {
+ mergeStyle = repo_model.MergeStyleMerge
+ } else if prConfig.AllowRebase {
+ mergeStyle = repo_model.MergeStyleRebase
+ } else if prConfig.AllowRebaseMerge {
+ mergeStyle = repo_model.MergeStyleRebaseMerge
+ } else if prConfig.AllowSquash {
+ mergeStyle = repo_model.MergeStyleSquash
+ } else if prConfig.AllowFastForwardOnly {
+ mergeStyle = repo_model.MergeStyleFastForwardOnly
+ } else if prConfig.AllowManualMerge {
+ mergeStyle = repo_model.MergeStyleManuallyMerged
+ }
+ if mergeStyle == "" {
+ return
+ }
+
+ // Check if there is a pending pr merge
+ hasPendingPullRequestMerge, pendingPullRequestMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
+ if err != nil {
+ ctx.ServerError("GetScheduledMergeByPullID", err)
+ return
+ }
+
+ var hasPendingPullRequestMergeTip template.HTML
+ if hasPendingPullRequestMerge {
+ createdPRMergeStr := templates.TimeSince(pendingPullRequestMerge.CreatedUnix)
+ hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr)
+ }
+
+ defaultMergeTitle, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
+ if err != nil {
+ ctx.ServerError("GetDefaultMergeMessage", err)
+ return
+ }
+ defaultSquashMergeTitle, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
+ if err != nil {
+ ctx.ServerError("GetDefaultSquashMergeMessage", err)
+ return
+ }
+
+ var defaultSquashMergeCommitMessages string
+ if !prInfo.IsPullRequestBroken {
+ defaultSquashMergeCommitMessages = pull_service.GetSquashMergeCommitMessages(ctx, pull)
+ }
+
+ allOverridableChecksOk := !prInfo.MergeBoxData.HasOverridableBlockers
+ prInfo.MergeBoxData.MergeFormProps = map[string]any{
+ "baseLink": prInfo.issue.Link(),
+ "textCancel": ctx.Locale.Tr("cancel"),
+ "textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget),
+ "textAutoMergeButtonWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_button_when_succeed"),
+ "textAutoMergeWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_when_succeed"),
+ "textAutoMergeCancelSchedule": ctx.Locale.Tr("repo.pulls.auto_merge_cancel_schedule"),
+ "textClearMergeMessage": ctx.Locale.Tr("repo.pulls.clear_merge_message"),
+ "textClearMergeMessageHint": ctx.Locale.Tr("repo.pulls.clear_merge_message_hint"),
+ "textMergeCommitId": ctx.Locale.Tr("repo.pulls.merge_commit_id"),
+
+ "canMergeNow": prInfo.MergeBoxData.CanMergeNow,
+ "allOverridableChecksOk": allOverridableChecksOk,
+ "emptyCommit": pull.IsEmpty(),
+ "pullHeadCommitID": prInfo.CompareInfo.HeadCommitID,
+ "isPullBranchDeletable": prInfo.MergeBoxData.isPullBranchDeletable,
+ "defaultMergeStyle": mergeStyle,
+ "defaultDeleteBranchAfterMerge": prConfig.DefaultDeleteBranchAfterMerge,
+ "mergeMessageFieldPlaceHolder": ctx.Locale.Tr("repo.editor.commit_message_desc"),
+ "defaultMergeMessage": defaultMergeBody,
+
+ "hasPendingPullRequestMerge": hasPendingPullRequestMerge,
+ "hasPendingPullRequestMergeTip": hasPendingPullRequestMergeTip,
+ }
+
+ // if this pr can be merged now, then hide the auto merge
+ generalHideAutoMerge := prInfo.MergeBoxData.CanMergeNow && allOverridableChecksOk
+
+ prInfo.MergeBoxData.MergeFormProps["mergeStyles"] = []any{
+ map[string]any{
+ "name": "merge",
+ "allowed": prConfig.AllowMerge,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"),
+ "mergeTitleFieldText": defaultMergeTitle,
+ "mergeMessageFieldText": defaultMergeBody,
+ "hideAutoMerge": generalHideAutoMerge,
+ },
+ map[string]any{
+ "name": "rebase",
+ "allowed": prConfig.AllowRebase,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"),
+ "hideMergeMessageTexts": true,
+ "hideAutoMerge": generalHideAutoMerge,
+ },
+ map[string]any{
+ "name": "rebase-merge",
+ "allowed": prConfig.AllowRebaseMerge,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"),
+ "mergeTitleFieldText": defaultMergeTitle,
+ "mergeMessageFieldText": defaultMergeBody,
+ "hideAutoMerge": generalHideAutoMerge,
+ },
+ map[string]any{
+ "name": "squash",
+ "allowed": prConfig.AllowSquash,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"),
+ "mergeTitleFieldText": defaultSquashMergeTitle,
+ "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody,
+ "hideAutoMerge": generalHideAutoMerge,
+ },
+ map[string]any{
+ "name": "fast-forward-only",
+ "allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"),
+ "hideMergeMessageTexts": true,
+ "hideAutoMerge": generalHideAutoMerge,
+ },
+ map[string]any{
+ "name": "manually-merged",
+ "allowed": prConfig.AllowManualMerge,
+ "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_manually"),
+ "hideMergeMessageTexts": true,
+ "hideAutoMerge": true,
+ },
+ }
+}
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index ace871a9f1..4a68c96aaa 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -40,8 +40,8 @@ func RenderFile(ctx *context.Context) {
defer blobReader.Close()
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{
CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(),
RenderQueryString: ctx.Req.URL.RawQuery,
diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go
index 8cfbfefdf1..c93c877d63 100644
--- a/routers/web/repo/star.go
+++ b/routers/web/repo/star.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/context"
)
-const tplStarUnstar templates.TplName = "repo/star_unstar"
+const tplStarUnstar templates.TplName = "repo/header/star"
func ActionStar(ctx *context.Context) {
err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "star")
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index 8d7721103a..f6c97e83bb 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -59,8 +59,8 @@ func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool)
func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Reader io.Reader) bool {
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
}).WithRelativePath(ctx.Repo.TreePath)
renderer := rctx.DetectMarkupRenderer(prefetchBuf)
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index 25e1f87806..79ca9efc36 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -190,8 +190,8 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(readmeFullPath),
+ CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(readmeFullPath),
}).WithRelativePath(readmeFullPath)
renderer := rctx.DetectMarkupRenderer(buf)
if renderer != nil {
diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go
index a7fbfc168b..616e1ee89c 100644
--- a/routers/web/repo/watch.go
+++ b/routers/web/repo/watch.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/context"
)
-const tplWatchUnwatch templates.TplName = "repo/watch_unwatch"
+const tplWatchUnwatch templates.TplName = "repo/header/watch"
func ActionWatch(ctx *context.Context) {
err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "watch")
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index faf2f442a2..b1d00520c2 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -251,7 +251,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
log.Error("failed to GetBlobContent: %v", err)
} else {
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
- CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
+ CurrentRefSubURL: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
})
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
log.Error("failed to RenderString: %v", err)
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index b2dc3f9840..3a0dff490a 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -132,14 +132,22 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS
}
func withScheduleInEventPayload(eventPayload, schedule string) string {
- if schedule == "" || eventPayload == "" {
+ if schedule == "" {
return eventPayload
}
- event := map[string]any{}
- if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
- log.Error("withScheduleInEventPayload: unmarshal: %v", err)
- return eventPayload
+ // eventPayload originates from json.Marshal(input.Payload) in handleSchedules,
+ // so a nil payload is stored as the literal "null" and pre-existing rows may be
+ // empty. Both cases start from a fresh map so the schedule field can still be set.
+ var event map[string]any
+ if eventPayload != "" {
+ if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
+ log.Error("withScheduleInEventPayload: unmarshal: %v", err)
+ return eventPayload
+ }
+ }
+ if event == nil {
+ event = map[string]any{}
}
event["schedule"] = schedule
diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go
index 770b842623..f2c7e656e6 100644
--- a/services/actions/schedule_tasks_test.go
+++ b/services/actions/schedule_tasks_test.go
@@ -22,9 +22,20 @@ func TestWithScheduleInEventPayload(t *testing.T) {
assert.Equal(t, "refs/heads/main", event["ref"])
})
- t.Run("keeps empty payload", func(t *testing.T) {
+ t.Run("adds schedule to null payload", func(t *testing.T) {
+ updated := withScheduleInEventPayload("null", "37 12 5 1 2")
+
+ event := map[string]any{}
+ assert.NoError(t, json.Unmarshal([]byte(updated), &event))
+ assert.Equal(t, "37 12 5 1 2", event["schedule"])
+ })
+
+ t.Run("adds schedule to empty payload", func(t *testing.T) {
updated := withScheduleInEventPayload("", "37 12 5 1 2")
- assert.Empty(t, updated)
+
+ event := map[string]any{}
+ assert.NoError(t, json.Unmarshal([]byte(updated), &event))
+ assert.Equal(t, "37 12 5 1 2", event["schedule"])
})
t.Run("keeps payload when schedule empty", func(t *testing.T) {
diff --git a/services/context/context_template.go b/services/context/context_template.go
index b63aaf4c3c..0f083d097e 100644
--- a/services/context/context_template.go
+++ b/services/context/context_template.go
@@ -5,13 +5,11 @@ package context
import (
"context"
- "fmt"
"html"
"html/template"
"net/http"
"strconv"
"strings"
- "sync"
"time"
"code.gitea.io/gitea/modules/httplib"
@@ -91,31 +89,14 @@ func (c TemplateContext) AppFullLink(link ...string) template.URL {
return template.URL(s + "/" + strings.TrimPrefix(link[0], "/"))
}
-var globalVars = sync.OnceValue(func() (ret struct {
- scriptImportRemainingPart string
-},
-) {
- // add onerror handler to alert users when the script fails to load:
- // * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config
- // * for developers: help them to remember to run "make watch-frontend" to build frontend assets
- // the message will be directly put in the onerror JS code's string
- onScriptErrorPrompt := `Please make sure the asset files can be accessed.`
- if !setting.IsProd {
- onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.`
- }
- onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt)
- ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `">`
- return ret
-})
-
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
if len(typ) > 0 {
if typ[0] == "module" {
- return template.HTML(``)
}
panic("unsupported script type: " + typ[0])
}
- return template.HTML(``)
}
func (c TemplateContext) CspScriptNonce() (ret string) {
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index 5a218bb62a..b7443345ad 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -11,5 +11,8 @@
{{template "base/footer_content" .}}
{{ctx.ScriptImport "js/index.js" "module"}}
{{template "custom/footer" .}}
+