0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-15 23:18:19 +02:00

Merge branch 'main' into main

This commit is contained in:
Karthik Bhandary 2026-04-28 10:08:12 +05:30 committed by GitHub
commit bb0da24a1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 441 additions and 438 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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, `

View File

@ -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 {

View File

@ -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")

View File

@ -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")
}

View File

@ -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 标签。了解详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">保护标签指南</a>。",
"repo.settings.tags.protection.pattern.description": "您可以使用单个名称或 glob 表达式匹配或正则表达式来匹配多个 Git 标签。了解详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">保护标签指南</a>。",
"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 钩子是默认配置,将被复制到所有新的仓库中。详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">Web 钩子指南</a>。",
"admin.defaulthooks.desc": "当某些 Gitea 事件触发时Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。欲了解详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">Web 钩子指南</a>。",
"admin.defaulthooks.add_webhook": "添加默认 Web 钩子",
"admin.defaulthooks.update_webhook": "更新默认Web钩子",
"admin.systemhooks": "系统 Web 钩子",
"admin.systemhooks.desc": "当某些 Gitea 事件触发时Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">Web 钩子指南</a>。",
"admin.systemhooks.desc": "当某些 Gitea 事件触发时Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 <a target=\"_blank\" rel=\"noopener\" href=\"%s\">Web 钩子指南</a>。",
"admin.systemhooks.add_webhook": "添加系统 Web 钩子",
"admin.systemhooks.update_webhook": "更新系统 Web 钩子",
"admin.auths.auth_manage_panel": "认证源管理",

View File

@ -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:

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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,
},
}
}

View File

@ -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,

View File

@ -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")

View File

@ -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)

View File

@ -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 {

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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) {

View File

@ -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) + `"></script>`
return ret
})
func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML {
if len(typ) > 0 {
if typ[0] == "module" {
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" type="module" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
}
panic("unsupported script type: " + typ[0])
}
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
return template.HTML(`<script nonce="` + c.CspScriptNonce() + `" src="` + html.EscapeString(public.AssetURI(path)) + `"></script>`)
}
func (c TemplateContext) CspScriptNonce() (ret string) {

View File

@ -11,5 +11,8 @@
{{template "base/footer_content" .}}
{{ctx.ScriptImport "js/index.js" "module"}}
{{template "custom/footer" .}}
<script nonce="{{ctx.CspScriptNonce}}" type="module">
if (!window.config?.frontendInited) alert("Frontend is not initialized, check console errors or asset files.")
</script>
</body>
</html>

View File

@ -1,13 +1,13 @@
<div class="ui container tw-flex">
{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
<div id="org-info" class="tw-flex tw-flex-col tw-flex-1 tw-break-anywhere">
<div class="ui header">
{{.Org.DisplayName}}
<span class="org-visibility">
<div class="ui container tw-flex tw-gap-4">
<div>{{ctx.AvatarUtils.Avatar .Org 100}}</div>
<div class="flex-relaxed-list">
<div class="ui header flex-left-right tw-m-0">
<div class="flex-text-block">
<span class="tw-text-2xl">{{.Org.DisplayName}}</span>
{{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
{{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
</span>
<span class="flex-text-block tw-ml-auto tw-text-16 tw-whitespace-nowrap">
</div>
<div class="flex-text-block">
{{if .EnableFeed}}
<a class="ui basic label button" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
{{svg "octicon-rss" 24}}
@ -16,14 +16,20 @@
{{if .IsSigned}}
{{template "org/follow_unfollow" .}}
{{end}}
</span>
</div>
</div>
{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}}
<div class="tw-text-text-light meta tw-mt-1">
{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
{{if .IsSigned}}
{{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}}
{{if .RenderedDescription}}
<div class="render-content markup">{{.RenderedDescription}}</div>
{{end}}
<div>
{{if .Org.Location}}
<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>
{{end}}
{{if .Org.Website}}
<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>
{{end}}
{{if and .IsSigned .Org.Email}}
<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>
{{end}}
</div>
</div>

View File

@ -55,11 +55,12 @@
{{end}}
{{if .NumMembers}}
<h4 class="ui top attached header tw-flex">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
<h4 class="ui top attached header flex-left-right">
<strong>{{ctx.Locale.Tr "org.members"}}</strong>
<a class="tw-text-text-light flex-text-inline" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
</h4>
<div class="ui attached segment members">
{{/* gap 8px below is specifically chosen to make sure a full line of avatars can exactly fit the segment width */}}
<div class="ui attached segment flex-text-block tw-flex-wrap tw-gap-[8px]">
{{range $memberUser := .OrgOverviewMembers}}
{{if or $.IsOrganizationMember (call $.IsPublicMember $memberUser.ID)}}
{{template "shared/user/avatarlink" dict "user" $memberUser "size" 32 "tooltip" true}}
@ -68,20 +69,22 @@
</div>
{{end}}
{{if .IsOrganizationMember}}
<div class="ui top attached header tw-flex">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
<div class="ui top attached header flex-left-right">
<strong>{{ctx.Locale.Tr "org.teams"}}</strong>
<a class="tw-text-text-light flex-text-inline" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
</div>
<div class="ui attached table segment teams">
<div class="ui attached segment">
<div class="flex-relaxed-list">
{{range .OrgOverviewTeams}}
<div class="item">
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong class="team-name">{{.Name}}</strong></a>
<div>
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
<p class="tw-text-text-light">
<a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.NumMembers}}</strong> {{ctx.Locale.Tr "org.lower_members"}}</a> ·
<a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/repositories"><strong>{{.NumRepos}}</strong> {{ctx.Locale.Tr "org.lower_repositories"}}</a>
</p>
</div>
{{end}}
</div>
</div>
{{if .IsOrganizationOwner}}
<div class="ui bottom attached segment">

View File

@ -1,18 +1,14 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization invite">
<div role="main" aria-label="{{.Title}}" class="page-content">
<div class="ui container">
{{template "base/alert" .}}
<div class="ui centered card">
<div class="image">
{{ctx.AvatarUtils.Avatar .Organization 140}}
</div>
<div class="content">
<div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}</div>
<div class="meta">{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}</div>
<div class="description">{{ctx.Locale.Tr "org.teams.invite.description"}}</div>
</div>
<div class="extra content">
<form class="ui form" action="" method="post">
<div class="tw-flex tw-justify-center">
<div class="flex-relaxed-list">
<div class="tw-flex tw-justify-center">{{ctx.AvatarUtils.Avatar .Organization 140}}</div>
<div>{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}</div>
<div>{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}</div>
<div>{{ctx.Locale.Tr "org.teams.invite.description"}}</div>
<form class="ui form tw-mt-4" action="" method="post">
<button class="fluid ui primary button">{{ctx.Locale.Tr "org.teams.join"}}</button>
</form>
</div>

View File

@ -15,21 +15,21 @@
{{end}}
</div>
</h4>
<div class="ui attached table segment detail">
<div class="item">
<div class="ui attached segment">
{{if .Team.Description}}
{{.Team.Description}}
{{else}}
<span class="tw-text-text-light tw-italic">{{ctx.Locale.Tr "org.teams.no_desc"}}</span>
{{end}}
</div>
{{if eq .Team.LowerName "owners"}}
<div class="item">
<div class="ui attached segment">
{{/* TODO: old indent is kept to make diff changes minimal, can be reformatted in the future */}}
{{if eq .Team.LowerName "owners"}}
<p>{{ctx.Locale.Tr "org.teams.owners_permission_desc"}}</p>
<p>{{ctx.Locale.Tr "org.teams.owners_permission_suggestion"}}</p>
</div>
{{else}}
<div class="item">
{{else}}
<h3>{{ctx.Locale.Tr "org.team_access_desc"}}</h3>
<ul>
{{if .Team.IncludesAllRepositories}}
@ -75,9 +75,9 @@
</tbody>
</table>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
{{if .IsOrganizationOwner}}
<div class="ui bottom attached segment">
<a class="ui small button" href="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/edit">{{svg "octicon-gear"}} {{ctx.Locale.Tr "org.teams.settings"}}</a>

View File

@ -57,59 +57,12 @@
{{svg "octicon-rss" 16}}
</a>
{{end}}
{{template "repo/watch_unwatch" $}}
{{template "repo/header/watch" $}}
{{if not $.DisableStars}}
{{template "repo/star_unstar" $}}
{{template "repo/header/star" $}}
{{end}}
{{if and (not .IsEmpty) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
<div class="ui labeled button
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
disabled
{{end}}"
{{if not $.IsSigned}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
{{end}}
>
<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
{{if not $.CanSignedUserFork}}
{{if gt (len $.UserAndOrgForks) 1}}
href="#" data-modal="#fork-repo-modal"
{{else if eq (len $.UserAndOrgForks) 1}}
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
{{end}}
{{else if not $.UserAndOrgForks}}
href="{{$.RepoLink}}/fork"
{{else}}
href="#" data-modal="#fork-repo-modal"
{{end}}
>
{{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span>
</a>
<a class="ui basic label" href="{{.Link}}/forks">
{{CountFmt .NumForks}}
</a>
</div>
<div class="ui small modal" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" .Name}}
</div>
<div class="content tw-text-left">
<div class="ui list">
{{range $.UserAndOrgForks}}
<div class="ui item tw-py-2">
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a>
</div>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
{{template "repo/header/fork" $}}
{{end}}
</div>
{{end}}

View File

@ -0,0 +1,47 @@
{{$canNotForkOwn := and $.IsSigned (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
<div class="ui labeled button"
{{if not $.IsSigned}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
{{else if $canNotForkOwn}}
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
{{end}}
>
<a role="button" class="ui compact small basic button {{if $.ShowForkModal}}show-modal{{end}}"
{{if not $.IsSigned}}
href="{{AppSubUrl}}/user/login"
{{else if $.ShowForkModal}}{{/* see backend comment for this logic */}}
href="#" data-modal="#fork-repo-modal"
{{else if eq (len $.UserAndOrgForks) 1}}
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
{{else if $canNotForkOwn}}
href="#"
{{else}}
href="{{$.RepoLink}}/fork"
{{end}}
>
{{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span>
</a>
<a class="ui basic label" href="{{$.Repository.Link}}/forks">
{{CountFmt $.Repository.NumForks}}
</a>
</div>
{{if $.ShowForkModal}}
<div class="ui small modal" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" $.Repository.Name}}
</div>
<div class="content">
<div class="ui relaxed list">
{{range $.UserAndOrgForks}}
<div class="item">
<a class="flex-text-block" href="{{.Link}}">{{svg "octicon-repo-forked"}}{{.FullName}}</a>
</div>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
{{end}}

View File

@ -1,18 +1,18 @@
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}>
{{$buttonText := ctx.Locale.Tr "repo.star"}}
{{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}}
<button type="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
<a role="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
{{if $.IsSigned}}
data-fetch-method="post"
data-fetch-url="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}"
data-fetch-sync="$closest(.ui.labeled.button)"
{{else}}
disabled
href="{{AppSubUrl}}/user/login"
{{end}}
>
{{svg (Iif $.IsStaringRepo "octicon-star-fill" "octicon-star")}}
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
</button>
</a>
<a class="ui basic label" href="{{$.RepoLink}}/stars">
{{CountFmt .Repository.NumStars}}
</a>

View File

@ -1,19 +1,19 @@
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
{{$buttonText := ctx.Locale.Tr "repo.watch"}}
{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
<button type="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
<a role="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
{{if $.IsSigned}}
data-fetch-method="post"
data-fetch-url="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"
data-fetch-sync="$closest(.ui.labeled.button)"
{{else}}
disabled
href="{{AppSubUrl}}/user/login"
{{end}}
>
{{svg "octicon-eye"}}
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
</button>
<a class="ui basic label" href="{{.RepoLink}}/watchers">
</a>
<a class="ui basic label" href="{{$.RepoLink}}/watchers">
{{CountFmt .Repository.NumWatches}}
</a>
</div>

View File

@ -208,100 +208,11 @@
{{end}}
{{if .AllowMerge}} {{/* user is allowed to merge */}}
{{$prUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypePullRequests}}
{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}}
{{$hasPendingPullRequestMergeTip := ""}}
{{if .HasPendingPullRequestMerge}}
{{$createdPRMergeStr := DateUtils.TimeSince .PendingPullRequestMerge.CreatedUnix}}
{{$hasPendingPullRequestMergeTip = ctx.Locale.Tr "repo.pulls.auto_merge_has_pending_schedule" .PendingPullRequestMerge.Doer.Name $createdPRMergeStr}}
{{end}}
{{if $data.MergeFormProps}}
<div class="divider"></div>
<script nonce="{{ctx.CspScriptNonce}}" type="module">
(() => {
const defaultMergeTitle = {{.DefaultMergeMessage}};
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
const defaultMergeMessage = {{.DefaultMergeBody}};
const defaultSquashMergeMessage = {{.DefaultSquashMergeBody}};
const mergeForm = {
'baseLink': {{.Issue.Link}},
'textCancel': {{ctx.Locale.Tr "cancel"}},
'textDeleteBranch': {{ctx.Locale.Tr "repo.branch.delete" .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': {{$canMergeNow}},
'allOverridableChecksOk': {{not $notAllOverridableChecksOk}},
'emptyCommit': {{.Issue.PullRequest.IsEmpty}},
'pullHeadCommitID': {{.PullHeadCommitID}},
'isPullBranchDeletable': {{.IsPullBranchDeletable}},
'defaultMergeStyle': {{.MergeStyle}},
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
'mergeMessageFieldPlaceHolder': {{ctx.Locale.Tr "repo.editor.commit_message_desc"}},
'defaultMergeMessage': defaultMergeMessage,
'hasPendingPullRequestMerge': {{.HasPendingPullRequestMerge}},
'hasPendingPullRequestMergeTip': {{$hasPendingPullRequestMergeTip}},
};
const generalHideAutoMerge = mergeForm.canMergeNow && mergeForm.allOverridableChecksOk; // if this pr can be merged now, then hide the auto merge
mergeForm['mergeStyles'] = [
{
'name': 'merge',
'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.merge_pull_request"}},
'mergeTitleFieldText': defaultMergeTitle,
'mergeMessageFieldText': defaultMergeMessage,
'hideAutoMerge': generalHideAutoMerge,
},
{
'name': 'rebase',
'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}},
'hideMergeMessageTexts': true,
'hideAutoMerge': generalHideAutoMerge,
},
{
'name': 'rebase-merge',
'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}},
'mergeTitleFieldText': defaultMergeTitle,
'mergeMessageFieldText': defaultMergeMessage,
'hideAutoMerge': generalHideAutoMerge,
},
{
'name': 'squash',
'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}},
'mergeTitleFieldText': defaultSquashMergeTitle,
'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage,
'hideAutoMerge': generalHideAutoMerge,
},
{
'name': 'fast-forward-only',
'allowed': {{and $prUnit.PullRequestsConfig.AllowFastForwardOnly (eq .Issue.PullRequest.CommitsBehind 0)}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}},
'hideMergeMessageTexts': true,
'hideAutoMerge': generalHideAutoMerge,
},
{
'name': 'manually-merged',
'allowed': {{$prUnit.PullRequestsConfig.AllowManualMerge}},
'textDoMerge': {{ctx.Locale.Tr "repo.pulls.merge_manually"}},
'hideMergeMessageTexts': true,
'hideAutoMerge': true,
}
];
window.config.pageData.pullRequestMergeForm = mergeForm;
})();
</script>
{{$showGeneralMergeForm = true}}
{{/* The merge form is a Vue component. After mounted, it has a button for choosing merge style, so make it have min-height to avoid layout shifting */}}
<div id="pull-request-merge-form" class="tw-min-h-[40px]"></div>
<div id="pull-request-merge-form" class="tw-min-h-[40px]" data-merge-form-props="{{JsonUtils.EncodeToString $data.MergeFormProps}}"></div>
{{else}}
{{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}}
<div class="divider"></div>
@ -396,8 +307,8 @@
</form>
{{end}}
{{if and .Issue.PullRequest.HeadRepo (not .Issue.PullRequest.HasMerged) (not .Issue.IsClosed)}}
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "ShowMergeInstructions" .ShowMergeInstructions}}
{{if $data.ShowPullCommands}}
{{template "repo/issue/view_content/pull_merge_instruction" dict "PullRequest" .Issue.PullRequest "MergeBoxData" $data}}
{{end}}
</div>
</div>

View File

@ -1,57 +1,59 @@
{{$data := $.MergeBoxData}}
{{$pull := $.PullRequest}}
<div class="divider"></div>
<details>
<summary>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}}</summary>
<div class="tw-mt-2">
<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div>
{{$localBranch := .PullRequest.HeadBranch}}
{{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}
{{$localBranch = print .PullRequest.HeadRepo.OwnerName "-" .PullRequest.HeadBranch}}
{{$localBranch := $pull.HeadBranch}}
{{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}
{{$localBranch = print $pull.HeadRepo.OwnerName "-" $pull.HeadBranch}}
{{end}}
<div class="ui secondary segment tw-font-mono">
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
{{if eq .PullRequest.Flow 0}}
<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}{{ctx.AppFullLink .PullRequest.HeadRepo.Link}}{{else}}{{$gitRemoteName}}{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
{{if eq $pull.Flow 0}}
<div>git fetch -u {{if ne $pull.HeadRepo.ID $pull.BaseRepo.ID}}{{ctx.AppFullLink $pull.HeadRepo.Link}}{{else}}{{$gitRemoteName}}{{end}} {{$pull.HeadBranch}}:{{$localBranch}}</div>
{{else}}
<div>git fetch -u {{$gitRemoteName}} {{.PullRequest.GetGitHeadRefName}}:{{$localBranch}}</div>
<div>git fetch -u {{$gitRemoteName}} {{$pull.GetGitHeadRefName}}:{{$localBranch}}</div>
{{end}}
<div>git checkout {{$localBranch}}</div>
</div>
{{if .ShowMergeInstructions}}
{{if $data.ShowMergeInstructions}}
<div>
<h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_title"}}</h3>
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_desc"}}
{{if not .AutodetectManualMerge}}
{{if not $data.AutodetectManualMerge}}
<div>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_warning"}}</div>
{{end}}
</div>
<div class="ui secondary segment tw-font-mono">
<div data-pull-merge-style="merge">
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --no-ff {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="rebase">
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --ff-only {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="rebase-merge">
<div>git checkout {{$localBranch}}</div>
<div>git rebase {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git rebase {{$pull.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --no-ff {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="squash">
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --squash {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="fast-forward-only">
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge --ff-only {{$localBranch}}</div>
</div>
<div class="tw-hidden" data-pull-merge-style="manually-merged">
<div>git checkout {{.PullRequest.BaseBranch}}</div>
<div>git checkout {{$pull.BaseBranch}}</div>
<div>git merge {{$localBranch}}</div>
</div>
<div>git push {{$gitRemoteName}} {{.PullRequest.BaseBranch}}</div>
<div>git push {{$gitRemoteName}} {{$pull.BaseBranch}}</div>
</div>
{{end}}
</div>

View File

@ -37,7 +37,6 @@
@import "./shared/flex-list.css";
@import "./shared/milestone.css";
@import "./shared/repoorg.css";
@import "./shared/settings.css";
@import "./features/dropzone.css";

View File

@ -1,23 +1,3 @@
.organization .head .ui.header .ui.right {
margin-top: 5px;
}
.page-content.organization .org-avatar {
margin-right: 15px;
}
.page-content.organization #org-info .ui.header {
display: flex;
align-items: center;
font-size: 36px;
margin-bottom: 0;
}
.page-content.organization #org-info .desc {
font-size: 16px;
margin-bottom: 10px;
}
.page-content.organization .team-item-box > .team-item-header {
min-height: 50px; /* the header sometimes contains a mini button, sometimes not, so we set a min-height to make sure the layout is consistent */
}
@ -30,20 +10,3 @@
white-space: nowrap;
color: var(--color-text-light-3);
}
.organization.invite .ui.avatar {
width: 100%;
height: 100%;
}
.organization.teams .detail .item {
padding: 10px 15px;
}
.organization.teams .detail .item:not(:last-child) {
border-bottom: 1px solid var(--color-secondary);
}
.org-team-navbar .active.item {
background: var(--color-box-body) !important;
}

View File

@ -1,18 +0,0 @@
.repository .head .ui.header .text,
.organization .head .ui.header .text {
vertical-align: middle;
font-size: 1.6rem;
margin-left: 15px;
}
.repository .ui.tabs.container,
.organization .ui.tabs.container {
margin-top: 14px;
margin-bottom: 0;
}
.repository .head .ui.header .org-visibility .label,
.organization .head .ui.header .org-visibility .label {
margin-left: 5px;
margin-top: 5px;
}

View File

@ -3,9 +3,11 @@ import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts';
const {pageData} = window.config;
const props = defineProps<{
mergeFormProps: any, // TODO: this is a huge object, need to be refactored in the future
}>();
const mergeForm = pageData.pullRequestMergeForm!;
const mergeForm = props.mergeFormProps;
const mergeTitleFieldValue = shallowRef('');
const mergeMessageFieldValue = shallowRef('');

View File

@ -63,27 +63,10 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) {
const el = box.querySelector('#pull-request-merge-form');
if (!el) return;
const data = JSON.parse(el.getAttribute('data-merge-form-props')!);
const {default: PullRequestMergeForm} = await import('../components/PullRequestMergeForm.vue');
const view = createApp(PullRequestMergeForm);
view.mount(el);
}
function executeScripts(elem: Element) {
// find any existing nonce value from the current page and apply it to the new script
const scriptNonce = document.querySelector('script[nonce]')!.getAttribute('nonce')!;
for (const oldScript of elem.querySelectorAll('script')) {
// TODO: that's the only way to load the data for the merge form. In the future
// we need to completely decouple the page data and embedded script
// eslint-disable-next-line github/no-dynamic-script-tag
const newScript = document.createElement('script');
for (const attr of oldScript.attributes) {
if (attr.name === 'type' && attr.value === 'module') continue;
newScript.setAttribute(attr.name, attr.value);
}
newScript.setAttribute('nonce', scriptNonce);
newScript.text = oldScript.text;
document.body.append(newScript);
}
const view = createApp(PullRequestMergeForm, {mergeFormProps: data});
view.mount(el); // TODO: can unmount when reloaded?
}
export function initRepoPullMergeBox(el: HTMLElement) {
@ -124,7 +107,6 @@ export function initRepoPullMergeBox(el: HTMLElement) {
}
document.removeEventListener('visibilitychange', onVisibilityChange);
const newElem = createElementFromHTML(await resp.text());
executeScripts(newElem);
el.replaceWith(newElem);
};

View File

@ -53,6 +53,7 @@ interface Window {
enableTimeTracking: boolean,
mermaidMaxSourceCharacters: number,
i18n: Record<string, string>,
frontendInited: boolean,
},
$: JQueryStatic,
jQuery: JQueryStatic,

View File

@ -171,3 +171,5 @@ const initDur = performance.now() - initStartTime;
if (initDur > 500) {
console.error(`slow init functions took ${initDur.toFixed(3)}ms`);
}
window.config.frontendInited = true;

View File

@ -12,6 +12,7 @@ window.config = {
enableTimeTracking: true,
mermaidMaxSourceCharacters: 5000,
i18n: {},
frontendInited: false,
};
window.testModules = {};