Merge branch 'main' into ssh-mirror-migrations

This commit is contained in:
pomidorry
2026-06-13 21:05:37 +03:00
committed by GitHub
58 changed files with 753 additions and 772 deletions
+41 -96
View File
@@ -151,9 +151,6 @@ func (d *delayWriter) WriteString(s string) (n int, err error) {
}
func (d *delayWriter) Close() error {
if d == nil {
return nil
}
stopped := d.timer.Stop()
if stopped || d.buf == nil {
return nil
@@ -163,16 +160,6 @@ func (d *delayWriter) Close() error {
return err
}
type nilWriter struct{}
func (n *nilWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil
}
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
fields := strings.Split(line, " ")
if len(fields) != 3 {
@@ -227,8 +214,7 @@ Gitea or set your environment appropriately.`, "")
total := 0
lastline := 0
var out io.Writer
out = &nilWriter{}
out := io.Discard
if setting.Git.VerbosePush {
if setting.Git.VerbosePushDelay > 0 {
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
@@ -350,12 +336,10 @@ Gitea or set your environment appropriately.`, "")
return nil
}
var out io.Writer
var dWriter *delayWriter
out = &nilWriter{}
out := io.Discard
if setting.Git.VerbosePush {
if setting.Git.VerbosePushDelay > 0 {
dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
defer dWriter.Close()
out = dWriter
} else {
@@ -382,101 +366,62 @@ Gitea or set your environment appropriately.`, "")
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
IsWiki: isWiki,
}
oldCommitIDs := make([]string, hookBatchSize)
newCommitIDs := make([]string, hookBatchSize)
refFullNames := make([]git.RefName, hookBatchSize)
count := 0
total := 0
wasEmpty := false
masterPushed := false
oldCommitIDs := make([]string, 0, hookBatchSize)
newCommitIDs := make([]string, 0, hookBatchSize)
refFullNames := make([]git.RefName, 0, hookBatchSize)
results := make([]private.HookPostReceiveBranchResult, 0)
defer func() {
hookPrintResults(results)
}()
processBatch := func() error {
if len(refFullNames) == 0 {
return nil
}
_, _ = fmt.Fprintf(out, " Processing %d references\n", len(refFullNames))
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if extra.HasError() {
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
}
results = append(results, resp.Results...)
oldCommitIDs = oldCommitIDs[:0]
newCommitIDs = newCommitIDs[:0]
refFullNames = refFullNames[:0]
return nil
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
// TODO: support news feeds for wiki
// wiki doesn't need "post-receive" at the moment
if isWiki {
continue
}
var ok bool
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
if !ok {
continue
}
_, _ = fmt.Fprintf(out, ".")
fmt.Fprintf(out, ".")
commitID, _ := git.NewIDFromString(newCommitIDs[count])
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
masterPushed = true
}
count++
total++
if count >= hookBatchSize {
fmt.Fprintf(out, " Processing %d references\n", count)
hookOptions.OldCommitIDs = oldCommitIDs
hookOptions.NewCommitIDs = newCommitIDs
hookOptions.RefFullNames = refFullNames
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if extra.HasError() {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
oldCommitIDs = append(oldCommitIDs, oldCommitID)
newCommitIDs = append(newCommitIDs, newCommitID)
refFullNames = append(refFullNames, refFullName)
if len(refFullNames) >= hookBatchSize {
// process and start a new batch
if err := processBatch(); err != nil {
return err
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
count = 0
}
}
if err := scanner.Err(); err != nil {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
}
if count == 0 {
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
if extra.HasError() {
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
}
}
fmt.Fprintf(out, "Processed %d references in total\n", total)
_ = dWriter.Close()
hookPrintResults(results)
return nil
}
hookOptions.OldCommitIDs = oldCommitIDs[:count]
hookOptions.NewCommitIDs = newCommitIDs[:count]
hookOptions.RefFullNames = refFullNames[:count]
fmt.Fprintf(out, " Processing %d references\n", count)
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
if resp == nil {
_ = dWriter.Close()
hookPrintResults(results)
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
}
wasEmpty = wasEmpty || resp.RepoWasEmpty
results = append(results, resp.Results...)
fmt.Fprintf(out, "Processed %d references in total\n", total)
if wasEmpty && masterPushed {
// We need to tell the repo to reset the default branch to master
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
if extra.HasError() {
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
}
}
_ = dWriter.Close()
hookPrintResults(results)
return nil
return processBatch()
}
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
+9 -3
View File
@@ -531,6 +531,10 @@ INTERNAL_TOKEN =
;;
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
;X_CONTENT_TYPE_OPTIONS = nosniff
;;
;; The value of the general Content-Security-Policy for most web pages.
;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy.
;CONTENT_SECURITY_POLICY_GENERAL =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2668,19 +2672,21 @@ LEVEL = Info
;FILE_EXTENSIONS = .adoc,.asciidoc
;; External command to render all matching extensions
;RENDER_COMMAND = "asciidoc --out-file=- -"
;; Don't pass the file on STDIN, pass the filename as argument instead.
;; Whether Gitea should write the content into a local temp file for the render command's input.
;; * false: the content will be passed via STDIN to the command.
;; * true: write the content into a local temp file, and pass the temp filename as argument to the command.
;IS_INPUT_FILE = false
;; How the content will be rendered.
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
;RENDER_CONTENT_MODE = sanitized
;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`.
;; The sandbox applied to the Content-Security-Policy for the rendered content when RENDER_CONTENT_MODE is `iframe`.
;; It defaults to a safe set of "allow-*" restrictions (space separated).
;; You can also set it by your requirements or use "disabled" to disable the sandbox completely.
;; When set it, make sure there is no security risk:
;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox.
;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions.
;; * HTML content with JS: do not set "allow-same-origin" unless the "RENDER_COMMAND" can guarantee there is no XSS.
;RENDER_CONTENT_SANDBOX =
;; Whether post-process the rendered HTML content, including:
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
-1
View File
@@ -15,7 +15,6 @@ import (
"gitea.dev/modules/setting"
// register supported doc types
_ "gitea.dev/modules/markup/asciicast"
_ "gitea.dev/modules/markup/console"
_ "gitea.dev/modules/markup/csv"
_ "gitea.dev/modules/markup/markdown"
+11
View File
@@ -482,6 +482,17 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
c.cmd.Stdout = c.cmdStdout
c.cmd.Stdin = c.cmdStdin
c.cmd.Stderr = c.cmdStderr
c.cmd.Cancel = func() error {
// Golang's default cmd.Cancel only calls Process.Kill(), but here we need to close the parent pipes together:
// * for some commands like "git --batch-xxx", Windows git might have 2 processes (a wrapper and a real git process)
// * on Windows, if parent process is killed (context canceled), the children process won't be killed, and the pipe handles are still open.
// * if we don't close the parent pipes here, the children process won't exit.
//
// There is no such problem on POSIX, while it won't make things worse by closing the parent pipes also on POSIX.
err := c.cmd.Process.Kill()
c.closePipeFiles(c.parentPipeFiles)
return err
}
return c.cmd.Start()
}
+1 -1
View File
@@ -168,7 +168,7 @@ func (ref RefName) ShortName() string {
if ref.IsFor() {
return ref.ForBranchName()
}
return string(ref) // usually it is a commit ID
return string(ref) // usually it is a commit ID, or "HEAD"
}
// RefGroup returns the group type of the reference
+3 -11
View File
@@ -36,25 +36,17 @@ func (repo *Repository) GetCommit(ref string) (*Commit, error) {
// GetBranchCommit returns the last commit of given branch.
func (repo *Repository) GetBranchCommit(name string) (*Commit, error) {
commitID, err := repo.GetBranchCommitID(name)
if err != nil {
return nil, err
}
return repo.GetCommit(commitID)
return repo.GetCommit(RefNameFromBranch(name).String())
}
// GetTagCommit get the commit of the specific tag via name
func (repo *Repository) GetTagCommit(name string) (*Commit, error) {
commitID, err := repo.GetTagCommitID(name)
if err != nil {
return nil, err
}
return repo.GetCommit(commitID)
return repo.GetCommit(RefNameFromTag(name).String())
}
func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) {
// File name starts with ':' must be escaped.
if relpath[0] == ':' {
if strings.HasPrefix(relpath, ":") {
relpath = `\` + relpath
}
+5 -1
View File
@@ -8,6 +8,7 @@ import (
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
@@ -86,8 +87,11 @@ func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName {
commit, err := repo.GetCommit(shortName)
if err == nil {
commitIDString := commit.ID.String()
if strings.HasPrefix(commitIDString, shortName) {
// make sure the "shortName" is either partial commit ID, or it is HEAD
if strings.HasPrefix(commitIDString, shortName) || shortName == RefNameHead {
return RefName(commitIDString)
} else {
setting.PanicInDevOrTesting("abuse of UnstableGuessRefByShortName, queried %s, got %s", shortName, commitIDString)
}
}
return ""
+1 -1
View File
@@ -40,7 +40,7 @@ type contextKey struct {
}
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
// The caller must call "defer gitRepo.Close()"
// The caller must call Closer.Close()
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
reqCtx := reqctx.FromContext(ctx)
if reqCtx != nil {
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package asciicast
import (
"fmt"
"io"
"net/url"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
)
func init() {
markup.RegisterRenderer(Renderer{})
}
// Renderer implements markup.Renderer for asciicast files.
// See https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md
type Renderer struct{}
func (Renderer) Name() string {
return "asciicast"
}
func (Renderer) FileNamePatterns() []string {
return []string{"*.cast"}
}
const (
playerClassName = "asciinema-player-container"
playerSrcAttr = "data-asciinema-player-src"
)
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}}
}
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
setting.AppSubURL,
url.PathEscape(ctx.RenderOptions.Metas["user"]),
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
url.PathEscape(ctx.RenderOptions.RelativePath),
)
return ctx.RenderInternal.FormatWithSafeAttrs(output, `<div class="%s" %s="%s"></div>`, playerClassName, playerSrcAttr, rawURL)
}
+5
View File
@@ -48,6 +48,11 @@ func RegisterRenderers() {
},
})
markup.RegisterRenderer(&frontendRenderer{
name: "asciicast",
patterns: []string{"*.cast"},
})
for _, renderer := range setting.ExternalMarkupRenderers {
markup.RegisterRenderer(&Renderer{renderer})
}
+3 -3
View File
@@ -5,6 +5,7 @@ package external
import (
"encoding/base64"
"errors"
"io"
"unicode/utf8"
@@ -54,14 +55,13 @@ func (p *frontendRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
func (p *frontendRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
ret.SanitizerDisabled = true
ret.DisplayInIframe = true
ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
ret.ContentSandbox = setting.MarkupRenderDefaultSandbox
return ret
}
func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
if ctx.RenderOptions.StandalonePageOptions == nil {
opts := p.GetExternalRendererOptions()
return markup.RenderIFrame(ctx, &opts, output)
return errors.New("should only be rendered in standalone page")
}
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
+5 -5
View File
@@ -211,11 +211,11 @@ func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.W
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
)
var extraAttrs template.HTML
if opts.ContentSandbox != "" {
extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, opts.ContentSandbox)
}
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" data-global-init="initExternalRenderIframe" class="external-render-iframe"%s></iframe>`, src, extraAttrs)
// The render response should always have correct "sandbox" limits (no same-origin),
// otherwise the "render link" direct access can still cause XSS without iframe.
// So here we do not need to set sandbox attribute on the iframe.
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" data-global-init="initExternalRenderIframe" class="external-render-iframe"></iframe>`, src)
return err
}
+2 -5
View File
@@ -22,10 +22,7 @@ func TestRenderIFrame(t *testing.T) {
WithRelativePath("tree-path").
WithMetas(map[string]string{"user": "test-owner", "repo": "test-repo", "RefTypeNameSubURL": "src/branch/master"})
// the value is read from config RENDER_CONTENT_SANDBOX, empty means "disabled"
ret := render(ctx, ExternalRendererOptions{ContentSandbox: ""})
// iframe doesn't need sandbox, the sandbox is set in render's response header
ret := render(ctx, ExternalRendererOptions{ContentSandbox: "any"})
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe"></iframe>`, ret)
ret = render(ctx, ExternalRendererOptions{ContentSandbox: "allow"})
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe" sandbox="allow"></iframe>`, ret)
}
+1 -3
View File
@@ -48,9 +48,7 @@ type SSHLogOption struct {
// HookPostReceiveResult represents an individual result from PostReceive
type HookPostReceiveResult struct {
Results []HookPostReceiveBranchResult
RepoWasEmpty bool
Err string
Results []HookPostReceiveBranchResult
}
// HookPostReceiveBranchResult represents an individual branch result from PostReceive
+6 -3
View File
@@ -13,9 +13,12 @@ type PushUpdateOptions struct {
PusherName string
RepoUserName string
RepoName string
RefFullName git.RefName // branch, tag or other name to push
OldCommitID string
NewCommitID string
// FIXME: this struct's design is not right, the changed commits should be in a separate slice
RefFullName git.RefName // branch, tag or other name to push
OldCommitID string
NewCommitID string
}
// IsNewRef return true if it's a first-time push to a branch, tag or etc.
+5 -3
View File
@@ -237,6 +237,10 @@ func fileExtensionsToPatterns(sectionName string, extensions []string) []string
return patterns
}
// MarkupRenderDefaultSandbox only contains a safe set of "sandbox allow" values, it is used to protect users from XSS attack,
// DO NOT USE "allow-same-origin" by default: if there is XSS in rendered content, same-origin makes the frame page can access parent window and send requests with user's credentials.
const MarkupRenderDefaultSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
func newMarkupRenderer(name string, sec ConfigSection) {
if !sec.Key("ENABLED").MustBool(false) {
return
@@ -269,9 +273,7 @@ func newMarkupRenderer(name string, sec ConfigSection) {
renderContentMode = RenderContentModeSanitized
}
// ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode.
// "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials.
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString(MarkupRenderDefaultSandbox)
if renderContentSandbox == "disabled" {
renderContentSandbox = ""
}
+6 -5
View File
@@ -18,6 +18,8 @@ var Security = struct {
// TODO: move more settings to this struct in future
XFrameOptions string
XContentTypeOptions string
ContentSecurityPolicyGeneral string // it only supports empty (default policy) or "unset", maybe it can support more in the future
}{
XFrameOptions: "SAMEORIGIN",
XContentTypeOptions: "nosniff",
@@ -150,13 +152,12 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0")
if sec.HasKey("X_FRAME_OPTIONS") {
Security.XFrameOptions = sec.Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
} else {
if !sec.HasKey("X_FRAME_OPTIONS") {
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
}
Security.XContentTypeOptions = sec.Key("X_CONTENT_TYPE_OPTIONS").MustString(Security.XContentTypeOptions)
if err := sec.MapTo(&Security); err != nil {
log.Fatal("Failed to map security settings: %v", err)
}
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
switch twoFactorAuth {
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadSecurityFrom(t *testing.T) {
cfg, err := NewConfigProviderFromData(`[security]
X_FRAME_OPTIONS = DENY
X_CONTENT_TYPE_OPTIONS = unset
CONTENT_SECURITY_POLICY_GENERAL = "script-src *; foo"`)
assert.NoError(t, err)
loadSecurityFrom(cfg)
assert.Equal(t, "DENY", Security.XFrameOptions)
assert.Equal(t, "unset", Security.XContentTypeOptions)
assert.Equal(t, `"script-src *`, Security.ContentSecurityPolicyGeneral) // holy shit ini package bug
}
+25 -3
View File
@@ -1321,6 +1321,7 @@
"repo.editor.fork_branch_exists": "分支「%s」已存在于您的派生仓库中,请选择一个新的分支名称。",
"repo.commits.desc": "浏览代码修改历史",
"repo.commits.commits": "次代码提交",
"repo.commits.history_enable_follow_renames": "包含重命名",
"repo.commits.no_commits": "没有共同的提交。「%s」和「%s」的历史完全不同。",
"repo.commits.nothing_to_compare": "没有差异可显示。",
"repo.commits.search.tooltip": "您可以在关键词前加上前缀,如「author:」、「committer:」、「after:」或「before:」,例如「retrin author:Alice before:2019-01-13」。",
@@ -2204,10 +2205,10 @@
"repo.settings.trust_model.collaborator.desc": "此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。",
"repo.settings.trust_model.committer": "提交者",
"repo.settings.trust_model.committer.long": "提交者: 信任与提交者相符的签名(这符合 GitHub 的行为并将强制 Gitea 签名的提交以 Gitea 为提交者)。",
"repo.settings.trust_model.committer.desc": "有效签名只有和提交者相匹配才会被标记为「受信任」,否则它们将被标记为「不匹配」。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。",
"repo.settings.trust_model.committer.desc": "有效签名只有和提交者相匹配才会被标记为「受信任」,否则它们将被标记为「不匹配」。这意味着在已签名的提交中,Gitea 必须作为提交者,而实际提交者则在提交信息中通过 `Co-authored-by:` 字段进行标注。 默认的 Gitea 密钥必须数据库中的用户相匹配。",
"repo.settings.trust_model.collaboratorcommitter": "协作者+提交者",
"repo.settings.trust_model.collaboratorcommitter.long": "协作者+提交者:信任协作者同时是提交者的签名",
"repo.settings.trust_model.collaboratorcommitter.desc": "此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这强制 Gitea 成为签名者和提交者,实际提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。",
"repo.settings.trust_model.collaboratorcommitter.desc": "此仓库中协作者的有效签名在他同时是提交者时将被标记为「受信任」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这强制使 Gitea 显示为已签名提交的提交者,实际提交者则在提交信息中以 `Co-Authored-By:` 尾注的形式标出。默认的 Gitea 密钥必须数据库中的用户相匹配。",
"repo.settings.wiki_delete": "删除百科数据",
"repo.settings.wiki_delete_desc": "删除仓库百科数据是永久性的,无法撤消。",
"repo.settings.wiki_delete_notices_1": "- 这将永久删除和禁用 %s 的百科。",
@@ -2598,6 +2599,9 @@
"repo.diff.review.reject": "请求变更",
"repo.diff.review.self_approve": "合并请求作者不能批准自己的合并请求",
"repo.diff.committed_by": "提交者",
"repo.diff.coauthored_by": "共同撰写人",
"repo.commits.avatar_stack_and": "和",
"repo.commits.avatar_stack_people": "%d 人",
"repo.diff.protected": "受保护的",
"repo.diff.image.side_by_side": "双排",
"repo.diff.image.swipe": "滑动",
@@ -2725,6 +2729,7 @@
"graphs.code_frequency.what": "代码频率",
"graphs.contributors.what": "贡献",
"graphs.recent_commits.what": "最近的提交",
"graphs.chart_zoom_hint": "拖动:缩放,Shift+拖动:平移,双击:重置缩放",
"org.org_name_holder": "组织名称",
"org.org_full_name_holder": "组织全名",
"org.org_name_helper": "组织名字应该简单明了。",
@@ -3772,6 +3777,7 @@
"actions.runs.no_matching_online_runner_helper": "没有匹配 %s 标签的在线运行器",
"actions.runs.no_job_without_needs": "工作流必须包含至少一个没有依赖关系的作业。",
"actions.runs.no_job": "工作流必须包含至少一个作业",
"actions.runs.invalid_reusable_workflow_uses": "无效的可复用工作流「uses」:%s",
"actions.runs.actor": "操作者",
"actions.runs.status": "状态",
"actions.runs.actors_no_select": "所有操作者",
@@ -3792,11 +3798,27 @@
"actions.runs.view_workflow_file": "查看工作流文件",
"actions.runs.summary": "摘要",
"actions.runs.all_jobs": "所有任务",
"actions.runs.job_summaries": "任务摘要",
"actions.runs.expand_caller_jobs": "显示此可复用工作流调用者的任务",
"actions.runs.collapse_caller_jobs": "隐藏此可复用工作流调用者的任务",
"actions.runs.attempt": "尝试",
"actions.runs.latest": "最新",
"actions.runs.latest_attempt": "最新尝试",
"actions.runs.triggered_via": "通过 %s 触发",
"actions.runs.total_duration": "总耗时:",
"actions.runs.rerun_triggered": "重新运行已触发",
"actions.runs.back_to_pull_request": "返回合并请求",
"actions.runs.back_to_workflow": "返回工作流",
"actions.runs.total_duration": "总耗时",
"actions.runs.workflow_dependencies": "工作流依赖项",
"actions.runs.graph_jobs_count_1": "%d 个任务",
"actions.runs.graph_jobs_count_n": "%d 个任务",
"actions.runs.graph_dependencies_count_1": "%d 个依赖项",
"actions.runs.graph_dependencies_count_n": "%d 个依赖项",
"actions.runs.graph_success_rate": "%s 成功",
"actions.runs.graph_zoom_in": "放大(在图上 Ctrl/Cmd + 滚动)",
"actions.runs.graph_zoom_max": "已为 100% 缩放",
"actions.runs.graph_zoom_out": "缩小(在图上 Ctrl/Cmd + 滚动)",
"actions.runs.graph_reset_view": "重置视图",
"actions.workflow.disable": "禁用工作流",
"actions.workflow.disable_success": "工作流「%s」已成功禁用。",
"actions.workflow.enable": "启用工作流",
+1 -1
View File
@@ -47,7 +47,7 @@
"cropperjs": "1.6.2",
"dayjs": "1.11.21",
"easymde": "2.21.0",
"esbuild": "0.28.0",
"esbuild": "0.28.1",
"idiomorph": "0.7.4",
"jquery": "4.0.0",
"js-yaml": "4.2.0",
+127 -127
View File
@@ -94,7 +94,7 @@ importers:
version: 2.6.2
'@vitejs/plugin-vue':
specifier: 6.0.7
version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))
version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))
ansi_up:
specifier: 6.0.6
version: 6.0.6
@@ -132,8 +132,8 @@ importers:
specifier: 2.21.0
version: 2.21.0
esbuild:
specifier: 0.28.0
version: 0.28.0
specifier: 0.28.1
version: 0.28.1
idiomorph:
specifier: 0.7.4
version: 0.7.4
@@ -193,10 +193,10 @@ importers:
version: 0.7.2
vite:
specifier: 8.0.16
version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)
vite-string-plugin:
specifier: 2.0.4
version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))
vue:
specifier: 3.5.35
version: 3.5.35(typescript@6.0.3)
@@ -257,7 +257,7 @@ importers:
version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
'@vitest/eslint-plugin':
specifier: 1.6.19
version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))
version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)))
eslint:
specifier: 10.4.1
version: 10.4.1(jiti@2.7.0)
@@ -347,7 +347,7 @@ importers:
version: 17.17.3
vitest:
specifier: 4.1.8
version: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
version: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))
vue-tsc:
specifier: 3.3.3
version: 3.3.3(typescript@6.0.3)
@@ -592,158 +592,158 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@esbuild/aix-ppc64@0.28.0':
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.0':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.0':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.0':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.0':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.0':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.0':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.0':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.0':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.0':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.0':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.0':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.0':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.0':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.0':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.0':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.0':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.0':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.0':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.0':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.0':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.0':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.0':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.0':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.0':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.0':
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -2498,8 +2498,8 @@ packages:
es-toolkit@1.47.0:
resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==}
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
esbuild@0.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
engines: {node: '>=18'}
hasBin: true
@@ -5352,82 +5352,82 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.28.0':
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.0':
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.0':
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.0':
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.0':
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.0':
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.0':
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.0':
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.0':
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.0':
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.0':
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.0':
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.0':
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.0':
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.0':
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.0':
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.0':
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.0':
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.0':
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.0':
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.0':
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.0':
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.0':
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.0':
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.0':
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.0':
'@esbuild/win32-x64@0.28.1':
optional: true
'@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.1(jiti@2.7.0))':
@@ -6358,13 +6358,13 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
'@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))':
'@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)
vue: 3.5.35(typescript@6.0.3)
'@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))':
'@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)))':
dependencies:
'@typescript-eslint/scope-manager': 8.60.1
'@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
@@ -6372,7 +6372,7 @@ snapshots:
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
typescript: 6.0.3
vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))
transitivePeerDependencies:
- supports-color
@@ -6385,13 +6385,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))':
'@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)
'@vitest/pretty-format@4.1.8':
dependencies:
@@ -7371,34 +7371,34 @@ snapshots:
es-toolkit@1.47.0: {}
esbuild@0.28.0:
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
'@esbuild/android-arm': 0.28.0
'@esbuild/android-arm64': 0.28.0
'@esbuild/android-x64': 0.28.0
'@esbuild/darwin-arm64': 0.28.0
'@esbuild/darwin-x64': 0.28.0
'@esbuild/freebsd-arm64': 0.28.0
'@esbuild/freebsd-x64': 0.28.0
'@esbuild/linux-arm': 0.28.0
'@esbuild/linux-arm64': 0.28.0
'@esbuild/linux-ia32': 0.28.0
'@esbuild/linux-loong64': 0.28.0
'@esbuild/linux-mips64el': 0.28.0
'@esbuild/linux-ppc64': 0.28.0
'@esbuild/linux-riscv64': 0.28.0
'@esbuild/linux-s390x': 0.28.0
'@esbuild/linux-x64': 0.28.0
'@esbuild/netbsd-arm64': 0.28.0
'@esbuild/netbsd-x64': 0.28.0
'@esbuild/openbsd-arm64': 0.28.0
'@esbuild/openbsd-x64': 0.28.0
'@esbuild/openharmony-arm64': 0.28.0
'@esbuild/sunos-x64': 0.28.0
'@esbuild/win32-arm64': 0.28.0
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
escalade@3.2.0: {}
@@ -10019,11 +10019,11 @@ snapshots:
core-util-is: 1.0.2
extsprintf: 1.3.0
vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)):
dependencies:
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)
vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0):
vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -10032,14 +10032,14 @@ snapshots:
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 25.9.1
esbuild: 0.28.0
esbuild: 0.28.1
fsevents: 2.3.3
jiti: 2.7.0
vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -10056,7 +10056,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.9.1
+169 -212
View File
@@ -5,6 +5,7 @@ package private
import (
"context"
"errors"
"fmt"
"net/http"
@@ -29,29 +30,8 @@ import (
repo_service "gitea.dev/services/repository"
)
// HookPostReceive updates services and users
func HookPostReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
// We don't rely on RepoAssignment here because:
// a) we don't need the git repo in this function
// OUT OF DATE: we do need the git repo to sync the branch to the db now.
// b) our update function will likely change the repository in the db so we will need to refresh it
// c) we don't always need the repo
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
// defer getting the repository at this point - as we should only retrieve it if we're going to call update
var (
repo *repo_model.Repository
gitRepo *git.Repository
)
defer gitRepo.Close() // it's safe to call Close on a nil pointer
func hookPostReceiveCollectPushUpdates(opts *private.HookOptions, repo *repo_model.Repository) []*repo_module.PushUpdateOptions {
updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
wasEmpty := false
for i := range opts.OldCommitIDs {
refFullName := opts.RefFullNames[i]
@@ -60,151 +40,124 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// or other less-standard refs spaces are ignored since there
// may be a very large number of them).
if refFullName.IsBranch() || refFullName.IsTag() {
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
// Error handled in loadRepository
return
}
wasEmpty = repo.IsEmpty
}
option := &repo_module.PushUpdateOptions{
RefFullName: refFullName,
OldCommitID: opts.OldCommitIDs[i],
NewCommitID: opts.NewCommitIDs[i],
PusherID: opts.UserID,
PusherName: opts.UserName,
RepoUserName: ownerName,
RepoName: repoName,
RepoUserName: repo.OwnerName,
RepoName: repo.Name,
}
updates = append(updates, option)
if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
// put the master/main branch first
// FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
// If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
// See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
// If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
copy(updates[1:], updates)
updates[0] = option
}
}
return updates
}
func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository, updates []*repo_module.PushUpdateOptions) bool {
branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
for _, update := range updates {
if !update.RefFullName.IsBranch() {
continue
}
if update.IsDelRef() {
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, fmt.Sprintf("failed to mark branch %s as deleted", update.RefFullName))
return false
}
} else {
branchesToSync = append(branchesToSync, update)
// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
pull_service.UpdatePullsRefs(ctx, repo, update)
}
}
if repo != nil && len(updates) > 0 {
branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
for _, update := range updates {
if !update.RefFullName.IsBranch() {
continue
}
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
return
}
wasEmpty = repo.IsEmpty
}
if update.IsDelRef() {
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
log.Error("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
} else {
branchesToSync = append(branchesToSync, update)
// TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
pull_service.UpdatePullsRefs(ctx, repo, update)
}
}
if len(branchesToSync) > 0 {
var err error
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
var (
branchNames = make([]string, 0, len(branchesToSync))
commitIDs = make([]string, 0, len(branchesToSync))
)
for _, update := range branchesToSync {
branchNames = append(branchNames, update.RefFullName.BranchName())
commitIDs = append(commitIDs, update.NewCommitID)
}
if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
}
if err := repo_service.PushUpdates(updates); err != nil {
log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
for i, update := range updates {
log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName())
}
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
if len(branchesToSync) == 0 {
return true
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to open repository")
return false
}
branchNames := make([]string, 0, len(branchesToSync))
commitIDs := make([]string, 0, len(branchesToSync))
for _, update := range branchesToSync {
branchNames = append(branchNames, update.RefFullName.BranchName())
commitIDs = append(commitIDs, update.NewCommitID)
}
if err = repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to sync branch to DB")
return false
}
return true
}
// HookPostReceive updates services and users
func HookPostReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
if opts.IsWiki {
setting.PanicInDevOrTesting("wiki hook-post-receive is not supported")
return
}
ownerName := ctx.PathParam("owner")
repoName := ctx.PathParam("repo")
repo := loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
return
}
// now, repo can't be nil
// first, collect updates and sync branches
updates := hookPostReceiveCollectPushUpdates(opts, repo)
if !hookPostReceiveSyncDatabaseBranches(ctx, opts, repo, updates) {
return
}
hookPostReceiveSyncRepoDefaultBranch(ctx, opts, repo)
// handle pull request merging, a pull request action should push at least 1 commit
if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase {
handlePullRequestMerging(ctx, opts, ownerName, repoName, updates)
if ctx.Written() {
if !hookPostReceiveHandlePullRequestMerging(ctx, opts, updates) {
return
}
}
if !hookPostReceiveUpdateRepoByOptions(ctx, opts, repo) {
return
}
// push async updates
if err := repo_service.PushUpdates(updates...); err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to push updates")
return
}
hookPostReceiveRespondWithTrailer(ctx, opts, repo)
}
func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) bool {
isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
// Handle Push Options
if isPrivate.Has() || isTemplate.Has() {
// load the repository
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
// Error handled in loadRepository
return
}
wasEmpty = repo.IsEmpty
}
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user")
return false
}
perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
return
ctx.PrivateError(http.StatusInternalServerError, err, "failed to load doer repo permission")
return false
}
if !perm.IsOwner() && !perm.IsAdmin() {
ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
Err: "Permissions denied",
})
return
ctx.PrivateError(http.StatusNotFound, nil, "permission denied")
return false
}
// FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag
@@ -213,22 +166,37 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
log.Error("failed to update repo is_private: %v", err)
}
}
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
repo.IsTemplate = isTemplate.Value()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"})
log.Error("failed to update repo is_template: %v", err)
}
}
}
return true
}
func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) {
results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs))
baseRepo := repo
if repo.IsFork {
if err := repo.GetBaseRepo(ctx); err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to load base repo")
return
}
if repo.BaseRepo.AllowsPulls(ctx) {
baseRepo = repo.BaseRepo
}
}
// We have to reload the repo in case its state is changed above
repo = nil
var baseRepo *repo_model.Repository
if !baseRepo.AllowsPulls(ctx) {
// We can stop there's no need to go any further
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{})
return
}
// Now handle the pull request notification trailers
for i := range opts.OldCommitIDs {
@@ -237,66 +205,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// If we've pushed a branch (and not deleted it)
if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
if ctx.Written() {
return
}
baseRepo = repo
if repo.IsFork {
if err := repo.GetBaseRepo(ctx); err != nil {
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
RepoWasEmpty: wasEmpty,
})
return
}
if repo.BaseRepo.AllowsPulls(ctx) {
baseRepo = repo.BaseRepo
}
}
if !baseRepo.AllowsPulls(ctx) {
// We can stop there's no need to go any further
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
RepoWasEmpty: wasEmpty,
})
return
}
}
branch := refFullName.BranchName()
if branch == baseRepo.DefaultBranch {
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
RepoID: repo.ID,
}); err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{Err: err.Error()})
return
}
if branch == baseRepo.DefaultBranch && !repo.IsFork {
// If our branch is the default branch of an unforked repo - there's no PR to create or refer to
if !repo.IsFork {
results = append(results, private.HookPostReceiveBranchResult{})
continue
}
results = append(results, private.HookPostReceiveBranchResult{})
continue
}
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf(
"Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err),
RepoWasEmpty: wasEmpty,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to get active PR for branch "+branch)
return
}
if pr == nil {
results = append(results, private.HookPostReceiveBranchResult{
Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx),
@@ -314,43 +235,79 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
}
}
}
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{
Results: results,
RepoWasEmpty: wasEmpty,
})
ctx.JSON(http.StatusOK, private.HookPostReceiveResult{Results: results})
}
func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) {
return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID)
}
// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) {
// hookPostReceiveHandlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit
func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, updates []*repo_module.PushUpdateOptions) bool {
if len(updates) == 0 {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID),
})
return
err := fmt.Errorf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID)
ctx.PrivateError(http.StatusInternalServerError, err, "no push update")
return false
}
pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID)
if err != nil {
log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"})
return
ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pull request")
return false
}
pusher, err := loadContextCacheUser(ctx, opts.UserID)
if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"})
return
ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user")
return false
}
// FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status
// here to keep it as before, that maybe PullRequestStatusMergeable
if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil {
log.Error("Failed to update PR to merged: %v", err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"})
_, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status)
if err != nil {
ctx.PrivateError(http.StatusInternalServerError, err, "failed to set pr to merged")
return false
}
return true
}
func hookPostReceiveSyncRepoDefaultBranch(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) {
hasBranch := false
for _, refFullName := range opts.RefFullNames {
if hasBranch = refFullName.IsBranch(); hasBranch {
break
}
}
if !hasBranch {
return
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
log.Error("failed to open git repo: %v", err)
return
}
// if default branch doesn't exist, try to guess one from existing git repo
_, err = gitRepo.GetBranchCommitID(repo.DefaultBranch)
if errors.Is(err, util.ErrNotExist) {
for _, guessBranchName := range []string{"main", "master"} {
if _, err = gitRepo.GetBranchCommitID(guessBranchName); err == nil {
repo.DefaultBranch = guessBranchName
err = repo_model.UpdateDefaultBranch(ctx, repo)
if err != nil {
log.Error("failed to update default branch: %v", err)
return
}
break
}
}
}
// if default branch was pushed, always keep the HEAD ref in sync
for _, refFullName := range opts.RefFullNames {
if refFullName.IsBranch() && refFullName.BranchName() == repo.DefaultBranch {
_ = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch)
}
}
}
+2 -2
View File
@@ -32,10 +32,10 @@ func TestHandlePullRequestMerging(t *testing.T) {
autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID})
ctx, resp := contexttest.MockPrivateContext(t, "/")
handlePullRequestMerging(ctx, &private.HookOptions{
hookPostReceiveHandlePullRequestMerging(ctx, &private.HookOptions{
PullRequestID: pr.ID,
UserID: 2,
}, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{
}, []*repo_module.PushUpdateOptions{
{NewCommitID: "01234567"},
})
assert.Empty(t, resp.Body.String())
+7 -4
View File
@@ -52,13 +52,16 @@ func Branches(ctx *context.Context) {
kw := ctx.FormString("q")
defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
defaultBranchOptional, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
if err != nil {
ctx.ServerError("LoadBranches", err)
return
}
commitIDs := []string{defaultBranch.DBBranch.CommitID}
commitIDs := make([]string, 0, len(branches)+1)
if defaultBranchOptional != nil {
commitIDs = append(commitIDs, defaultBranchOptional.DBBranch.CommitID)
}
for _, branch := range branches {
commitIDs = append(commitIDs, branch.DBBranch.CommitID)
}
@@ -83,7 +86,7 @@ func Branches(ctx *context.Context) {
ctx.Data["Branches"] = branches
ctx.Data["CommitStatus"] = commitStatus
ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["DefaultBranchBranch"] = defaultBranch
ctx.Data["DefaultBranchBranch"] = defaultBranchOptional
pager := context.NewPagination(branchesCount, pageSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -152,7 +155,7 @@ func RestoreBranchPost(ctx *context.Context) {
objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
// Don't return error below this
if err := repo_service.PushUpdate(
if err := repo_service.PushUpdates(
&repo_module.PushUpdateOptions{
RefFullName: git.RefNameFromBranch(deletedBranch.Name),
OldCommitID: objectFormat.EmptyObjectID().String(),
+22 -21
View File
@@ -58,8 +58,6 @@ func CorsHandler() func(next http.Handler) http.Handler {
// httpBase does the common work for git http services,
// including early response, authentication, repository lookup and permission check.
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if ctx.FormString("go-get") == "1" {
context.EarlyResponseForGoGetMeta(ctx)
return nil
@@ -93,11 +91,11 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
isWiki := false
unitType := unit.TypeCode
if strings.HasSuffix(reponame, ".wiki") {
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if strings.HasSuffix(repoName, ".wiki") {
isWiki = true
unitType = unit.TypeWiki
reponame = reponame[:len(reponame)-5]
repoName = repoName[:len(repoName)-5]
}
owner := ctx.ContextUser
@@ -107,14 +105,14 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
}
repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame)
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
ctx.ServerError("GetRepositoryByName", err)
return nil
}
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil
}
@@ -127,23 +125,26 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
// don't allow anonymous pulls if organization is not public
if isPublicPull {
if err := repo.LoadOwner(ctx); err != nil {
ctx.ServerError("LoadOwner", err)
return nil
// Only public pulls don't need auth: repo must exist, not require-sign-in
canAnonymousPull := false
if isPull && repoExist && !setting.Service.RequireSignInViewStrict {
// allow anonymous pulls if owner is public and repo is public (not private)
if owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate {
canAnonymousPull = true
}
// then check "public anonymous access" permission
if !canAnonymousPull && ctx.Doer == nil {
anonPerm, err := access_model.GetDoerRepoPermission(ctx, repo, nil)
if err != nil {
ctx.ServerError("GetDoerRepoPermission", err)
return nil
}
canAnonymousPull = anonPerm.CanAccess(accessMode, unitType)
}
askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
}
// check access
if askAuth {
// rely on the results of Contexter
if !canAnonymousPull { // not public pull, then either the pull needs auth, or the push needs "write" permission, so ask auth
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
if setting.OAuth2.Enabled {
@@ -229,7 +230,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, reponame)
repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, repoName)
if err != nil {
log.Error("pushCreateRepo: %v", err)
ctx.Status(http.StatusNotFound)
+1 -3
View File
@@ -63,9 +63,7 @@ func RenderFile(ctx *context.Context) {
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
extRendererOpts := extRenderer.GetExternalRendererOptions()
if extRendererOpts.ContentSandbox != "" {
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox "+extRendererOpts.ContentSandbox)
} else {
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
ctx.Resp.Header().Add("Content-Security-Policy", "sandbox "+extRendererOpts.ContentSandbox)
}
err = markup.RenderWithRenderer(rctx, renderer, rendererInput, ctx.Resp)
-17
View File
@@ -18,7 +18,6 @@ import (
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
@@ -63,22 +62,6 @@ func MustBeAbleToUpload(ctx *context.Context) {
}
}
func CommitInfoCache(ctx *context.Context) {
var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.ServerError("GetBranchCommit", err)
return
}
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
}
func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
if err != nil {
+2 -4
View File
@@ -1482,15 +1482,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
m.Post("/generate-notes", web.Bind(forms.GenerateReleaseNotesForm{}), repo.GenerateReleaseNotes)
m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter)
m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache)
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
// end "/{username}/{reponame}": repo releases
+3
View File
@@ -115,6 +115,9 @@ func (c TemplateContext) CspScriptNonce() (ret string) {
}
func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
if setting.Security.ContentSecurityPolicyGeneral == "unset" {
return "" // if site admin disables the general CSP, then we don't use it
}
// The CSP problem is more complicated than it looks.
// Gitea was designed to support various "customizations", including:
// * custom themes (custom CSS and JS)
+9
View File
@@ -9,6 +9,7 @@ import (
"time"
"gitea.dev/modules/graceful"
"gitea.dev/modules/private"
"gitea.dev/modules/process"
"gitea.dev/modules/web"
web_types "gitea.dev/modules/web/types"
@@ -49,6 +50,14 @@ func (ctx *PrivateContext) Err() error {
return ctx.Base.Err()
}
func (ctx *PrivateContext) PrivateError(status int, err error, userMsg string) {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
ctx.JSON(status, private.Response{Err: errMsg, UserMsg: userMsg})
}
type privateContextKeyType struct{}
var privateContextKey privateContextKeyType
+1 -4
View File
@@ -972,12 +972,9 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName)
if err == nil {
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
} else {
// if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
log.Error("GetBranchCommit: %v", err)
} else {
ctx.ServerError("GetBranchCommit", err)
return
}
} else { // there is a path in request
guessLegacyPath := refType == ""
+11 -10
View File
@@ -59,9 +59,9 @@ type Branch struct {
}
// LoadBranches loads branches from the repository limited by page & pageSize.
func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
if err != nil {
func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (defaultBranchOptional *Branch, _ []*Branch, _ int64, _ error) {
defaultDBBranchOptional, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return nil, nil, 0, err
}
@@ -108,13 +108,14 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
branches = append(branches, branch)
}
// Always add the default branch
log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name)
defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo)
if err != nil {
return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
if defaultDBBranchOptional != nil {
// Always add the default branch
defaultBranchOptional, err = loadOneBranch(ctx, repo, defaultDBBranchOptional, &rules, repoIDToRepo, repoIDToGitRepo)
if err != nil {
return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
}
}
return defaultBranch, branches, totalNumOfBranches, nil
return defaultBranchOptional, branches, totalNumOfBranches, nil
}
func getDivergenceCacheKey(repoID int64, branchName string) string {
@@ -640,7 +641,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
func deleteBranchSuccessPostProcess(doer *user_model.User, repo *repo_model.Repository, branchName string, branchCommit *git.Commit) {
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
if err := PushUpdate(
if err := PushUpdates(
&repo_module.PushUpdateOptions{
RefFullName: git.RefNameFromBranch(branchName),
OldCommitID: branchCommit.ID.String(),
+2 -1
View File
@@ -47,7 +47,8 @@ func NewTemporaryUploadRepository(repo *repo_model.Repository) (*TemporaryUpload
// Close the repository cleaning up all files
func (t *TemporaryUploadRepository) Close() {
defer t.gitRepo.Close()
// must stop the repo access before removal, otherwise Windows can't remove the directory occupied by other processes
t.gitRepo.Close()
if t.cleanup != nil {
t.cleanup()
}
+29 -38
View File
@@ -32,10 +32,26 @@ import (
// pushQueue represents a queue to handle update pull request tests
var pushQueue *queue.WorkerPoolQueue[[]*repo_module.PushUpdateOptions]
// handle passed PR IDs and test the PRs
func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions {
func initPushQueue() error {
pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", pushQueueHandler)
if pushQueue == nil {
return errors.New("unable to create push_update queue")
}
go graceful.GetManager().RunWithCancel(pushQueue)
return nil
}
// PushUpdates adds a push update to push queue, each call must pass the same repo updates
func PushUpdates(opts ...*repo_module.PushUpdateOptions) error {
if len(opts) == 0 {
return nil
}
return pushQueue.Push(opts)
}
func pushQueueHandler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions {
for _, opts := range items {
if err := pushUpdates(opts); err != nil {
if err := pushQueueHandleUpdates(opts); err != nil {
// Username and repository stays the same between items in opts.
pushUpdate := opts[0]
log.Error("pushUpdate[%s/%s] failed: %v", pushUpdate.RepoUserName, pushUpdate.RepoName, err)
@@ -44,37 +60,8 @@ func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpd
return nil
}
func initPushQueue() error {
pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", handler)
if pushQueue == nil {
return errors.New("unable to create push_update queue")
}
go graceful.GetManager().RunWithCancel(pushQueue)
return nil
}
// PushUpdate is an alias of PushUpdates for single push update options
func PushUpdate(opts *repo_module.PushUpdateOptions) error {
return PushUpdates([]*repo_module.PushUpdateOptions{opts})
}
// PushUpdates adds a push update to push queue
func PushUpdates(opts []*repo_module.PushUpdateOptions) error {
if len(opts) == 0 {
return nil
}
for _, opt := range opts {
if opt.IsNewRef() && opt.IsDelRef() {
return errors.New("Old and new revisions are both NULL")
}
}
return pushQueue.Push(opts)
}
// pushUpdates generates push action history feeds for push updating multiple refs
func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
// pushQueueHandleUpdates generates push action history feeds for push updating multiple refs
func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error {
if len(optsList) == 0 {
return nil
}
@@ -94,7 +81,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
defer gitRepo.Close()
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
return fmt.Errorf("Failed to update size for repository: %v", err)
return fmt.Errorf("failed to update size for repository: %v", err)
}
addTags := make([]string, 0, len(optsList))
@@ -104,10 +91,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
for _, opts := range optsList {
log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName)
if opts.IsNewRef() && opts.IsDelRef() {
return fmt.Errorf("old and new revisions are both %s", objectFormat.EmptyObjectID())
setting.PanicInDevOrTesting("invalid push update (add+del): %+v", opts)
continue
}
if opts.RefFullName.IsTag() {
if pusher == nil || pusher.ID != opts.PusherID {
if opts.PusherID == user_model.ActionsUserID {
@@ -188,11 +176,14 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
return err
}
// delete cache for divergence
// sync branch related database data
if branch == repo.DefaultBranch {
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
log.Error("DelRepoDivergenceFromCache: %v", err)
}
if err := AddRepoToLicenseUpdaterQueue(&LicenseUpdaterOptions{RepoID: repo.ID}); err != nil {
log.Error("AddRepoToLicenseUpdaterQueue: %v", err)
}
} else {
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
log.Error("DelDivergenceFromCache: %v", err)
+6 -2
View File
@@ -4,7 +4,6 @@
<div class="ui container">
{{template "base/alert" .}}
{{template "repo/sub_menu" .}}
{{if .DefaultBranchBranch}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.default_branch"}}
{{if and $.IsWriter $.Repository.CanContentChange (not .IsDeleted)}}
@@ -14,7 +13,8 @@
{{end}}
</h4>
<div class="ui attached table segment">
{{if .DefaultBranchBranch}}
<div class="ui attached table segment" data-testid="branches-default-branch-list">
<table class="ui very basic fixed table single line">
<tbody>
<tr>
@@ -69,6 +69,10 @@
</tbody>
</table>
</div>
{{else}}
<div class="ui attached segment" data-testid="branches-default-branch-not-exist">
{{ctx.Locale.Tr "repo.branch.default_branch_not_exist" $.Repository.DefaultBranch}}
</div>
{{end}}
<h4 class="ui top attached header">
+4 -4
View File
@@ -6,14 +6,14 @@
{{$commitActionsStatuses := ctx.ActionsUtils.CommitStatusesToActionsStatuses $.CommitStatuses}}
{{range $cs := $.CommitStatuses}}
<div class="item commit-status-item">
<div class="flex-text-block">
<div class="flex-text-block tw-flex-1">
{{$actionStatus := $commitActionsStatuses.IconStatus $cs}}
{{if $actionStatus}}
{{template "repo/icons/action_status" (dict "Status" $actionStatus "Size" 18 "ClassName" "commit-status icon")}}
{{else}}
{{template "repo/icons/commit_status" $cs}}
{{end}}
<div class="status-context gt-ellipsis">
<div class="gt-ellipsis">
{{$cs.Context}} <span class="tw-text-text-light-2">{{$cs.Description}}</span>
</div>
</div>
@@ -29,9 +29,9 @@
{{end}}
{{range $missingCheck := $statusCheckData.MissingRequiredChecks}}
<div class="item commit-status-item">
<div class="flex-text-block">
<div class="flex-text-block tw-flex-1">
{{svg "octicon-dot-fill" 16 "commit-status icon tw-text-yellow"}}
<div class="status-context gt-ellipsis">{{$missingCheck}}</div>
<div class="gt-ellipsis">{{$missingCheck}}</div>
</div>
<div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div>
</div>
+17 -11
View File
@@ -2,7 +2,8 @@ import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {apiCreateRepo, apiCreateFile, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
test('3d model file', async ({page, request}) => {
test('3d model file', async ({page, request, browserName}) => {
test.skip(browserName === 'firefox', 'unclear firefox-only CI-only failure'); // eslint-disable-line playwright/no-skipped-test
const repoName = `e2e-3d-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
@@ -13,7 +14,7 @@ test('3d model file', async ({page, request}) => {
await expect(iframe).toBeVisible();
const frame = page.frameLocator('iframe.external-render-iframe');
const viewer = frame.locator('#frontend-render-viewer');
await expect(viewer.locator('canvas')).toBeVisible();
await expect(viewer.locator('canvas')).toBeVisible(); // unclear firefox-only CI-only failure
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
await assertFlushWithParent(iframe, page.locator('.file-view'));
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
@@ -39,19 +40,24 @@ test('pdf file', async ({page, request}) => {
});
test('asciicast file', async ({page, request}) => {
// regression for repo_file.go's RefTypeNameSubURL double-escape: readme.cast on a non-ASCII branch
// is rendered via view_readme.go (no metas override), exposing the bug as a broken player URL
const repoName = `e2e-asciicast-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
const branch = '日本語-branch';
const branchEnc = encodeURIComponent(branch);
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n';
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "test-content"]\n';
// on an empty repo, apiCreateFile with newBranch creates that branch as the initial commit
await apiCreateFile(request, owner, repoName, 'readme.cast', cast, {newBranch: branch});
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`);
const container = page.locator('.asciinema-player-container');
await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`);
await expect(container.locator('.ap-wrapper')).toBeVisible();
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
await apiCreateFile(request, owner, repoName, 'test.cast', cast, {newBranch: branch});
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}/test.cast`);
const iframe = page.locator('iframe.external-render-iframe');
const frame = iframe.contentFrame();
const viewer = frame.locator('#frontend-render-viewer[data-frontend-render-name]');
await expect(viewer).toHaveAttribute('data-frontend-render-name', 'asciicast'); // render succeeded
await expect(viewer).toHaveAttribute('data-window-origin', 'null'); // no same-origin, avoid XSS
const wrapper = frame.locator('.ap-wrapper');
await expect(wrapper).toBeVisible();
await expect(wrapper).toContainText('test-content');
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
await assertFlushWithParent(iframe, page.locator('.file-view'));
await assertNoJsError(page);
});
+38 -38
View File
@@ -8,6 +8,9 @@ import (
"net/url"
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/translation"
"gitea.dev/tests"
@@ -18,55 +21,52 @@ import (
func TestViewBranches(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
req := NewRequest(t, "GET", "/user2/repo1/branches")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
_, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url")
assert.False(t, exists, "The template has changed")
}
AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-list]", 1)
AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-not-exist]", 0)
func TestDeleteBranch(t *testing.T) {
defer tests.PrepareTestEnv(t)()
deleteBranch(t)
repo1.DefaultBranch = "non-existent-branch"
_, _ = db.GetEngine(t.Context()).ID(repo1.ID).Cols("default_branch").Update(repo1)
req = NewRequest(t, "GET", "/user2/repo1/branches")
resp = MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-list]", 0)
AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-not-exist]", 1)
}
func TestUndoDeleteBranch(t *testing.T) {
branchAction := func(t *testing.T, button string) (*HTMLDoc, string) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/branches")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find(button).Attr("data-url")
require.True(t, exists, "The template has changed")
linkURL, err := url.Parse(link)
require.NoError(t, err)
req = NewRequest(t, "POST", link)
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/user2/repo1/branches")
resp = session.MakeRequest(t, req, http.StatusOK)
return NewHTMLParser(t, resp.Body), linkURL.Query().Get("name")
}
onGiteaRun(t, func(t *testing.T, u *url.URL) {
deleteBranch(t)
htmlDoc, name := branchAction(t, ".restore-branch-button")
htmlDoc, name := branchAction(t, ".delete-branch-button")
assert.Contains(t,
htmlDoc.doc.Find(".ui.positive.message").Text(),
translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name),
)
htmlDoc, name = branchAction(t, ".restore-branch-button")
assert.Contains(t,
htmlDoc.doc.Find(".ui.positive.message").Text(),
translation.NewLocale("en-US").TrString("repo.branch.restore_success", name),
)
})
}
func deleteBranch(t *testing.T) {
htmlDoc, name := branchAction(t, ".delete-branch-button")
assert.Contains(t,
htmlDoc.doc.Find(".ui.positive.message").Text(),
translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name),
)
}
func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/branches")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find(button).Attr("data-url")
require.True(t, exists, "The template has changed")
req = NewRequest(t, "POST", link)
session.MakeRequest(t, req, http.StatusOK)
url, err := url.Parse(link)
assert.NoError(t, err)
req = NewRequest(t, "GET", "/user2/repo1/branches")
resp = session.MakeRequest(t, req, http.StatusOK)
return NewHTMLParser(t, resp.Body), url.Query().Get("name")
}
+9 -1
View File
@@ -33,9 +33,17 @@ func TestCompareTag(t *testing.T) {
// A dropdown for both base and head.
assert.Lenf(t, selection.Nodes, 2, "The template has changed")
req = NewRequest(t, "GET", "/user2/repo1/compare/v1.1...HEAD")
resp = session.MakeRequest(t, req, http.StatusOK)
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
req = NewRequest(t, "GET", "/user2/repo1/compare/v1.1...NotExisting").SetHeader("Accept", "text/html")
resp = session.MakeRequest(t, req, http.StatusNotFound)
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
req = NewRequest(t, "GET", "/user2/repo1/compare/invalid").SetHeader("Accept", "text/html")
resp = session.MakeRequest(t, req, http.StatusNotFound)
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "expect 404 page not 500")
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
}
// Compare with inferred default branch (master)
+34
View File
@@ -10,7 +10,9 @@ import (
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
@@ -26,6 +28,8 @@ func TestGitSmartHTTP(t *testing.T) {
testGitSmartHTTPTokenScopes(t)
testRenamedRepoRedirect(t)
testGitArchiveRemote(t, u)
t.Run("AnonymousAccess-Repo", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, false) })
t.Run("AnonymousAccess-Wiki", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, true) })
})
}
@@ -144,3 +148,33 @@ func testGitArchiveRemote(t *testing.T, u *url.URL) {
t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test"))
t.Run("list compression options", doGitRemoteArchive(u.String(), "--list"))
}
// testGitSmartHTTPPrivateRepoAnonymousAccess tests that a private repo with
// anonymous code access enabled can be cloned without credentials.
func testGitSmartHTTPPrivateRepoAnonymousAccess(t *testing.T, isWiki bool) {
// repo1 (ID=1) belongs to user2 and is public by default in fixtures
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerName: "user2", Name: "repo1"})
unitType := util.Iif(isWiki, unit.TypeWiki, unit.TypeCode)
repoLink := "/" + repo.FullName() + util.Iif(isWiki, ".wiki", "")
gitPullPath := repoLink + "/info/refs?service=git-upload-pack"
gitPushPath := repoLink + "/info/refs?service=git-receive-pack"
// make the repo private
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: repo.ID, IsPrivate: true}, "is_private"))
// without anonymous access: anonymous pull must require auth
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized)
// enable anonymous read access on the unit
require.NoError(t, repo_model.UpdateRepoUnitPublicAccess(t.Context(), &repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, AnonymousAccessMode: perm.AccessModeRead}))
// with anonymous code access: anonymous pull must succeed without credentials
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusOK)
// push (receive-pack) must still require auth even with anonymous code access
MakeRequest(t, NewRequest(t, "GET", gitPushPath), http.StatusUnauthorized)
// RequireSignInViewStrict must override anonymous access
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized)
}
+7 -10
View File
@@ -96,17 +96,17 @@ func TestExternalMarkupRenderer(t *testing.T) {
iframe := NewHTMLParser(t, respParent.Body).Find("iframe.external-render-iframe")
assert.Empty(t, iframe.AttrOr("src", "")) // src should be empty, "data-src" is used instead
// default sandbox on parent page
assert.Equal(t, "allow-scripts allow-popups", iframe.AttrOr("sandbox", ""))
// no sandbox on parent page because the rendered response should always have correct sandbox
assert.Equal(t, "(non-existing)", iframe.AttrOr("sandbox", "(non-existing)"))
assert.Equal(t, "/user2/repo1/render/branch/master/test.html", iframe.AttrOr("data-src", ""))
})
t.Run("SubPage", func(t *testing.T) {
t.Run("FramePage", func(t *testing.T) {
req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/test.html")
respSub := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", respSub.Header().Get("Content-Type"))
// default sandbox in sub page response
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy"))
// default sandbox in sub-page response (there should be no "allow-same-origin")
assert.Equal(t, "sandbox allow-scripts allow-forms allow-modals allow-popups allow-downloads", respSub.Header().Get("Content-Security-Policy"))
// FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "<script>" tag, but it indeed is the sanitizer's job
assert.Equal(t,
`<script nonce crossorigin src="`+public.AssetURI("web_src/js/external-render-helper.ts")+`" id="gitea-external-render-helper" data-render-query-string=""></script>`+
@@ -127,10 +127,7 @@ func TestExternalMarkupRenderer(t *testing.T) {
req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/bin.no-sanitizer")
respSub := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, binaryContent, respSub.Body.String()) // raw content should keep the raw bytes (including invalid UTF-8 bytes), and no "external-render-iframe" helpers
// no sandbox (disabled by RENDER_CONTENT_SANDBOX)
assert.Empty(t, iframe.AttrOr("sandbox", ""))
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
assert.Empty(t, respSub.Header().Get("Content-Security-Policy"), "sandbox is disabled by RENDER_CONTENT_SANDBOX")
})
t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) {
@@ -142,7 +139,7 @@ func TestExternalMarkupRenderer(t *testing.T) {
`<script>foo("raw")</script>`,
respSub.Body.String(),
)
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
assert.Empty(t, respSub.Header().Get("Content-Security-Policy"))
})
})
})
-1
View File
@@ -5,7 +5,6 @@ RUN_MODE = prod
[database]
DB_TYPE = sqlite3
PATH = gitea-test.db
SQLITE_JOURNAL_MODE = WAL
[indexer]
REPO_INDEXER_ENABLED = true
Vendored
+1 -1
View File
@@ -50,7 +50,7 @@ declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
declare module 'asciinema-player' {
interface AsciinemaPlayer {
create(src: string, element: HTMLElement, options?: Record<string, unknown>): void;
create(src: string | {data: string}, element: HTMLElement, options?: Record<string, unknown>): void;
}
const exports: AsciinemaPlayer;
export = exports;
+4 -1
View File
@@ -840,7 +840,8 @@ table th[data-sortt-desc] .svg {
align-items: center;
gap: var(--gap-inline);
vertical-align: middle;
min-width: 0; /* make ellipsis work */
max-width: 100%; /* the inner part might have "gt-ellipsis" */
min-width: 0; /* if it is the top flex container, "max-width" works; but if it is inside another flex container, the parent needs to handle the x-axis and here also needs "min-width" */
}
.ui.ui.labeled.button {
@@ -856,6 +857,7 @@ table th[data-sortt-desc] .svg {
justify-content: space-between;
align-items: center;
gap: var(--gap-block);
max-width: 100%;
min-width: 0;
}
@@ -868,6 +870,7 @@ table th[data-sortt-desc] .svg {
align-items: center;
gap: var(--gap-block);
max-width: 100%;
min-width: 0;
}
.flex-left-right > .ui.button,
-1
View File
@@ -52,7 +52,6 @@
@import "./markup/content.css";
@import "./markup/codeblock.css";
@import "./markup/codepreview.css";
@import "./markup/asciicast.css";
@import "./font_i18n.css";
@import "./base.css";
-10
View File
@@ -1,10 +0,0 @@
.asciinema-player-container {
width: 100%;
height: auto;
}
/* Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js : <div class="ap-term" ...>
Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 */
.ap-term {
overflow: hidden !important;
}
+2 -6
View File
@@ -210,10 +210,6 @@ td .commit-summary {
overflow: auto;
}
.non-diff-file-content .asciicast {
padding: 0 !important;
}
.repo-editor-header {
/* it should match ".repo-button-row" so the tree toggle button stays aligned */
margin: 8px 0;
@@ -1867,11 +1863,11 @@ tbody.commit-list {
}
.username-display {
max-width: 100%; /* the inner part might have "gt-ellipsis" */
min-width: 0; /* if it is the top flex container, "max-width" works; but if it is inside another flex container, the parent needs to handle the x-axis and here also needs "min-width" */
display: inline-flex;
gap: var(--gap-inline);
align-items: center;
max-width: 100%; /* min/max widths are for "gt-ellipsis", see the comment of other "flex-xxx" family classes */
min-width: 0;
}
.username-display > .username-fullname {
@@ -1953,8 +1953,8 @@ $.fn.dropdown = function(parameters) {
$choice.find(selector.menu).remove();
$choice.find(selector.menuIcon).remove();
}
return ($choice.data(metadata.text) !== undefined)
? $choice.data(metadata.text)
return ($choice.attr('data-' + metadata.text) !== undefined) // GITEA-PATCH: use "attr" but not "data", don't decode JSON like "false"
? $choice.attr('data-' + metadata.text)
: (preserveHTML)
? $choice.html().trim()
: $choice.text().trim()
@@ -2007,8 +2007,8 @@ $.fn.dropdown = function(parameters) {
value = ( $option.attr('value') !== undefined )
? $option.attr('value')
: name,
text = ( $option.data(metadata.text) !== undefined )
? $option.data(metadata.text)
text = ( $option.attr('data-' + metadata.text) !== undefined ) // GITEA-PATCH: use "attr" but not "data", don't decode JSON like "false"
? $option.attr('data-' + metadata.text)
: name,
group = $option.parent('optgroup')
;
+11 -5
View File
@@ -8,6 +8,7 @@ type LazyLoadFunc = () => Promise<{frontendRender: FrontendRenderFunc}>;
const frontendPlugins: Record<string, LazyLoadFunc> = {
'viewer-3d': () => import('./render/plugins/frontend-viewer-3d.ts'),
'openapi-swagger': () => import('./render/plugins/frontend-openapi-swagger.ts'),
'asciicast': () => import('./render/plugins/frontend-asciicast.ts'),
};
class Options implements FrontendRenderOptions {
@@ -44,23 +45,28 @@ async function initFrontendExternalRender() {
const viewerContainer = document.querySelector<HTMLElement>('#frontend-render-viewer')!;
const renderNames = viewerContainer.getAttribute('data-frontend-renders')!.split(' ');
const fileTreePath = viewerContainer.getAttribute('data-file-tree-path')!;
viewerContainer.setAttribute('data-window-origin', window.origin); // mainly for testing purpose
const fileDataElem = document.querySelector<HTMLTextAreaElement>('#frontend-render-data')!;
fileDataElem.remove();
const fileDataContent = fileDataElem.value;
const fileDataEncoding = fileDataElem.getAttribute('data-content-encoding')!;
const opts = new Options(viewerContainer, fileTreePath, fileDataEncoding, fileDataContent);
let found = false;
let renderName = '', rendered = false;
for (const name of renderNames) {
if (!(name in frontendPlugins)) continue;
const plugin = await frontendPlugins[name]();
found = true;
if (await plugin.frontendRender(opts)) break;
renderName = name;
rendered = await plugin.frontendRender(opts);
if (rendered) break;
}
if (!found) {
if (!renderName) {
viewerContainer.textContent = 'No frontend render plugin found for this file, but backend declares that there must be one, there must be a bug';
} else if (!rendered) {
viewerContainer.textContent = `Failed to render by ${renderName}`;
} else {
viewerContainer.setAttribute('data-frontend-render-name', renderName); // succeeded render, mainly for testing purpose
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import './external-render-helper.ts';
test('isValidCssColor', async () => {
const isValidCssColor = window.testModules.externalRenderHelper!.isValidCssColor;
const isValidCssColor = window.giteaExternalRenderHelper!.isValidCssColor;
expect(isValidCssColor(null)).toBe(false);
expect(isValidCssColor('')).toBe(false);
+6 -8
View File
@@ -50,12 +50,12 @@ body { background: ${backgroundColor}; }
}
const iframeId = queryParams.get('gitea-iframe-id');
if (iframeId) {
// iframe is in different origin, so we need to use postMessage to communicate
const postIframeMsg = (cmd: string, data: Record<string, any> = {}) => {
window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*');
};
// iframe is in different origin, so we need to use postMessage to communicate
const postIframeMsg = (cmd: string, data: Record<string, any> = {}) => {
window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*');
};
if (iframeId) {
const updateIframeHeight = () => {
if (!document.body) return; // the body might not be available when this function is called
// Use scrollHeight to get the full content height, even when CSS sets html/body to height:100%
@@ -90,6 +90,4 @@ if (iframeId) {
});
}
if (window.testModules) {
window.testModules.externalRenderHelper = {isValidCssColor};
}
window.giteaExternalRenderHelper = {isValidCssColor, queryParams, postIframeMsg};
+5 -5
View File
@@ -68,13 +68,13 @@ interface Window {
turnstile: any,
hcaptcha: any,
// Make IIFE private functions can be tested in unit tests, without exposing the IIFE module to global scope.
// Make IIFE private functions can be managed by us in our scope, without exposing the IIFE module to global scope.
// Otherwise, when using "export" in IIFE code, the compiled JS will inject global "var externalRenderHelper = ..."
// which is not expected and may cause conflicts with other modules.
testModules: {
externalRenderHelper?: {
isValidCssColor(s: string | null): boolean,
}
giteaExternalRenderHelper?: {
isValidCssColor(s: string | null): boolean,
queryParams: URLSearchParams,
postIframeMsg(cmd: string, data: Record<string, any> = {}),
}
// do not add more properties here unless it is a must
-16
View File
@@ -1,16 +0,0 @@
import {queryElems} from '../utils/dom.ts';
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
const [player] = await Promise.all([
import('asciinema-player'),
import('asciinema-player/dist/bundle/asciinema-player.css'),
]);
player.create(el.getAttribute('data-asciinema-player-src')!, el, {
// poster (a preview frame) to display until the playback is started.
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
poster: 'npt:1:0:0',
});
});
}
-2
View File
@@ -1,7 +1,6 @@
import {initMarkupCodeMermaid} from './mermaid.ts';
import {initMarkupCodeMath} from './math.ts';
import {initMarkupCodeCopy} from './codecopy.ts';
import {initMarkupRenderAsciicast} from './asciicast.ts';
import {initMarkupTasklist} from './tasklist.ts';
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
import {initExternalRenderIframe} from './render-iframe.ts';
@@ -24,6 +23,5 @@ export function initMarkupContent(): void {
initMarkupTasklist(el);
initMarkupCodeMermaid(el);
initMarkupCodeMath(el);
initMarkupRenderAsciicast(el);
});
}
+27 -6
View File
@@ -1,7 +1,6 @@
import {generateElemId} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {isDarkTheme} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
function safeRenderIframeLink(link: any): string | null {
try {
@@ -65,9 +64,31 @@ export async function initExternalRenderIframe(iframe: HTMLIFrameElement) {
u.searchParams.set('gitea-iframe-id', iframe.id);
u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe));
// It must use "srcdoc" here, because our backend always sends CSP sandbox directive for the rendered content
// (to protect from XSS risks), so we can't use "src" to load the content directly, otherwise there will be console errors like:
// Unsafe attempt to load URL http://localhost:3000/test from frame with URL http://localhost:3000/test
const resp = await GET(u.href);
iframe.srcdoc = await resp.text();
// There are 3 kinds of external render modes:
// * external frontend render:
// * parent page creates iframe, iframe navigates to render page
// * render generates frame page with external-render-helper (injected), external-render-frontend and file content (hidden textarea)
// * frame page executes external-render-frontend JS code to finds a frontend plugin to render
// * external backend render (HTML)
// * parent page creates iframe, iframe navigates to render page
// * render executes command to generate rendered HTML content with external-render-helper (injected)
// * frame page displays the rendered content
// * external backend render (non-HTML, e.g.: PDF, image)
// * parent page creates iframe, iframe navigates to render page
// * render executes command to generate rendered content
// * response header is automatically detected from rendered content
// It must use "src" here, because the frame content should not inherit parent's CSP.
// Otherwise, "srcdoc" makes the frame content inherit the parent's CSP,
// then some renders like "asciicast (asciinema)" which require "unsafe-eval" won't work.
//
// When using "src", Chrome can report false-alarm error like:
// * Unsafe attempt to load URL http://localhost/owner/repo/render/branch/main/file from frame with URL http://localhost/owner/repo/render/branch/main/file. Domains, protocols and ports must match.
// (only for the first time that the developer opens the browser console)
// Such error log can also appear even if you access the link "http://.../owner/repo/render/branch/main/file" directly.
// Everything just works, it is just a false-alarm caused by Chrome's Developer Tools, so such error log can be ignored.
//
// Another reason for why "src" is a must: if the render outputs non-HTML contents like PDF or image,
// Only "src" can correctly load and display the rendered content, "srcdoc" won't work.
iframe.src = u.href;
}
@@ -1,6 +1,23 @@
import '../../../fomantic/build/fomantic.js';
import {createElementFromHTML} from '../../utils/dom.ts';
import {hideScopedEmptyDividers} from './dropdown.ts';
test('dropdown-item-literal-text', () => {
// a "choice" workflow_dispatch input can offer the string "false" as an option.
// jQuery `.data()` would coerce `data-text="false"` to the boolean `false`, which then renders as empty text.
const $dropdown = $(`<select class="ui dropdown">
<option value="1">1</option>
<option value="0">0</option>
<option value="true">true</option>
<option value="false">false</option>
</select>`).dropdown();
for (const value of ['1', '0', 'true', 'false']) {
$dropdown.dropdown('set selected', value);
expect($dropdown.dropdown('get text')).toEqual(value);
expect($dropdown.dropdown('get value')).toEqual(value);
}
});
test('hideScopedEmptyDividers-simple', () => {
const container = createElementFromHTML(`<div>
<div class="divider"></div>
@@ -0,0 +1,23 @@
import type {FrontendRenderFunc} from '../plugin.ts';
export const frontendRender: FrontendRenderFunc = async (opts): Promise<boolean> => {
try {
const [player] = await Promise.all([
import('asciinema-player'),
import('asciinema-player/dist/bundle/asciinema-player.css'),
]);
player.create({data: opts.contentString()}, opts.container, {
// poster (a preview frame) to display until the playback is started.
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
poster: 'npt:1:0:0',
});
// Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js : <div class="ap-term" ...>
// Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159
opts.container.querySelector<HTMLElement>('.ap-term')!.style.overflow = 'hidden';
opts.container.querySelector<HTMLElement>('.ap-player')!.style.borderRadius = '0';
return true;
} catch (error) {
console.error(error);
return false;
}
};
-2
View File
@@ -14,5 +14,3 @@ window.config = {
i18n: {},
frontendInited: false,
};
window.testModules = {};