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:
commit
bb0da24a1f
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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, `
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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": "认证源管理",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
147
routers/web/repo/pull_merge_form.go
Normal file
147
routers/web/repo/pull_merge_form.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
47
templates/repo/header/fork.tmpl
Normal file
47
templates/repo/header/fork.tmpl
Normal 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}}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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('');
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
1
web_src/js/globals.d.ts
vendored
1
web_src/js/globals.d.ts
vendored
@ -53,6 +53,7 @@ interface Window {
|
||||
enableTimeTracking: boolean,
|
||||
mermaidMaxSourceCharacters: number,
|
||||
i18n: Record<string, string>,
|
||||
frontendInited: boolean,
|
||||
},
|
||||
$: JQueryStatic,
|
||||
jQuery: JQueryStatic,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -12,6 +12,7 @@ window.config = {
|
||||
enableTimeTracking: true,
|
||||
mermaidMaxSourceCharacters: 5000,
|
||||
i18n: {},
|
||||
frontendInited: false,
|
||||
};
|
||||
|
||||
window.testModules = {};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user