mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-04 04:22:15 +02:00
Merge branch 'main' into ssh-mirror-migrations
This commit is contained in:
+41
-96
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Vendored
+5
@@ -48,6 +48,11 @@ func RegisterRenderers() {
|
||||
},
|
||||
})
|
||||
|
||||
markup.RegisterRenderer(&frontendRenderer{
|
||||
name: "asciicast",
|
||||
patterns: []string{"*.cast"},
|
||||
})
|
||||
|
||||
for _, renderer := range setting.ExternalMarkupRenderers {
|
||||
markup.RegisterRenderer(&Renderer{renderer})
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+127
-127
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 == ""
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')
|
||||
;
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
Vendored
+5
-5
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -14,5 +14,3 @@ window.config = {
|
||||
i18n: {},
|
||||
frontendInited: false,
|
||||
};
|
||||
|
||||
window.testModules = {};
|
||||
|
||||
Reference in New Issue
Block a user