mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-24 13:53:42 +09:00
Compare commits
5 Commits
1f32170060
...
c5332fdc55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5332fdc55 | ||
|
|
0f668145e9 | ||
|
|
fbe80e6df2 | ||
|
|
151ef80e28 | ||
|
|
8106d95577 |
6
.github/workflows/pull-db-tests.yml
vendored
6
.github/workflows/pull-db-tests.yml
vendored
@@ -72,13 +72,13 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
- run: GOEXPERIMENT='' make backend
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
- name: run migration tests
|
||||
run: make test-sqlite-migration
|
||||
- name: run tests
|
||||
run: make test-sqlite
|
||||
run: GOEXPERIMENT='' make test-sqlite
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
RACE_ENABLED: true
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: make unit-test-coverage test-check
|
||||
run: GOEXPERIMENT='' make unit-test-coverage test-check
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
RACE_ENABLED: true
|
||||
|
||||
6
Makefile
6
Makefile
@@ -18,6 +18,10 @@ DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
IMPORT := code.gitea.io/gitea
|
||||
|
||||
# By default use go's 1.25 experimental json v2 library when building
|
||||
# TODO: remove when no longer experimental
|
||||
export GOEXPERIMENT ?= jsonv2
|
||||
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
|
||||
@@ -766,7 +770,7 @@ generate-go: $(TAGS_PREREQ)
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||
|
||||
8
go.mod
8
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
|
||||
gitea.com/go-chi/cache v0.2.1
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
|
||||
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
github.com/42wim/httpsig v1.2.3
|
||||
@@ -61,6 +61,7 @@ require (
|
||||
github.com/go-redsync/redsync/v4 v4.13.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-webauthn/webauthn v0.13.4
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
@@ -75,7 +76,6 @@ require (
|
||||
github.com/huandu/xstrings v1.5.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
github.com/jhillyerd/enmime v1.3.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
@@ -200,7 +200,6 @@ require (
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.24 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
@@ -220,6 +219,7 @@ require (
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
@@ -277,7 +277,7 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -43,8 +43,8 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
|
||||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
|
||||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
|
||||
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15 h1:qFYmz05u/s9664o7+XEgrlHXSPQ4uHO8/ccZGUb1uxA=
|
||||
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
||||
@@ -848,8 +848,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
|
||||
@@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error {
|
||||
if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
|
||||
jsonBuf, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = output.Write([]byte{'\n'})
|
||||
_, err = output.Write(jsonBuf)
|
||||
_, err = output.Write(bytes.TrimSpace(jsonBuf))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Encoder represents an encoder for json
|
||||
@@ -31,71 +29,7 @@ type Interface interface {
|
||||
Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultJSONHandler default json handler
|
||||
DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
|
||||
|
||||
_ Interface = StdJSON{}
|
||||
_ Interface = JSONiter{}
|
||||
)
|
||||
|
||||
// StdJSON implements Interface via encoding/json
|
||||
type StdJSON struct{}
|
||||
|
||||
// Marshal implements Interface
|
||||
func (StdJSON) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal implements Interface
|
||||
func (StdJSON) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// NewEncoder implements Interface
|
||||
func (StdJSON) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
// NewDecoder implements Interface
|
||||
func (StdJSON) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
|
||||
// Indent implements Interface
|
||||
func (StdJSON) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return json.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
|
||||
// JSONiter implements Interface via jsoniter
|
||||
type JSONiter struct {
|
||||
jsoniter.API
|
||||
}
|
||||
|
||||
// Marshal implements Interface
|
||||
func (j JSONiter) Marshal(v any) ([]byte, error) {
|
||||
return j.API.Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal implements Interface
|
||||
func (j JSONiter) Unmarshal(data []byte, v any) error {
|
||||
return j.API.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// NewEncoder implements Interface
|
||||
func (j JSONiter) NewEncoder(writer io.Writer) Encoder {
|
||||
return j.API.NewEncoder(writer)
|
||||
}
|
||||
|
||||
// NewDecoder implements Interface
|
||||
func (j JSONiter) NewDecoder(reader io.Reader) Decoder {
|
||||
return j.API.NewDecoder(reader)
|
||||
}
|
||||
|
||||
// Indent implements Interface, since jsoniter don't support Indent, just use encoding/json's
|
||||
func (j JSONiter) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return json.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
var DefaultJSONHandler = getDefaultJSONHandler()
|
||||
|
||||
// Marshal converts object as bytes
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) {
|
||||
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{
|
||||
> "a": 1
|
||||
>}`, buf.String())
|
||||
}
|
||||
|
||||
35
modules/json/jsongoccy.go
Normal file
35
modules/json/jsongoccy.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var _ Interface = jsonGoccy{}
|
||||
|
||||
type jsonGoccy struct{}
|
||||
|
||||
func (jsonGoccy) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonGoccy) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (jsonGoccy) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (jsonGoccy) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return json.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
22
modules/json/jsonlegacy.go
Normal file
22
modules/json/jsonlegacy.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !goexperiment.jsonv2
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func getDefaultJSONHandler() Interface {
|
||||
return jsonGoccy{}
|
||||
}
|
||||
|
||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||
return DefaultJSONHandler.Marshal(v)
|
||||
}
|
||||
|
||||
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
|
||||
return DefaultJSONHandler.NewDecoder(reader)
|
||||
}
|
||||
34
modules/json/jsonv1.go
Normal file
34
modules/json/jsonv1.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
)
|
||||
|
||||
type jsonV1 struct{}
|
||||
|
||||
var _ Interface = jsonV1{}
|
||||
|
||||
func (jsonV1) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonV1) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (jsonV1) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (jsonV1) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func (jsonV1) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return json.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
92
modules/json/jsonv2.go
Normal file
92
modules/json/jsonv2.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build goexperiment.jsonv2
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
|
||||
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
)
|
||||
|
||||
// JSONv2 implements Interface via encoding/json/v2
|
||||
// Requires GOEXPERIMENT=jsonv2 to be set at build time
|
||||
type JSONv2 struct {
|
||||
marshalOptions jsonv2.Options
|
||||
marshalKeepOptionalEmptyOptions jsonv2.Options
|
||||
unmarshalOptions jsonv2.Options
|
||||
unmarshalCaseInsensitiveOptions jsonv2.Options
|
||||
}
|
||||
|
||||
var jsonV2 JSONv2
|
||||
|
||||
func init() {
|
||||
commonMarshalOptions := []jsonv2.Options{
|
||||
jsonv2.FormatNilSliceAsNull(true),
|
||||
jsonv2.FormatNilMapAsNull(true),
|
||||
}
|
||||
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
|
||||
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
|
||||
|
||||
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
|
||||
// v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept.
|
||||
// Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2
|
||||
jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...)
|
||||
|
||||
// Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig)
|
||||
jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true))
|
||||
}
|
||||
|
||||
func getDefaultJSONHandler() Interface {
|
||||
return &jsonV2
|
||||
}
|
||||
|
||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) Marshal(v any) ([]byte, error) {
|
||||
return jsonv2.Marshal(v, j.marshalOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) Unmarshal(data []byte, v any) error {
|
||||
return jsonv2.Unmarshal(data, v, j.unmarshalOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) NewEncoder(writer io.Writer) Encoder {
|
||||
return &jsonV2Encoder{writer: writer, opts: j.marshalOptions}
|
||||
}
|
||||
|
||||
func (j *JSONv2) NewDecoder(reader io.Reader) Decoder {
|
||||
return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions}
|
||||
}
|
||||
|
||||
// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet)
|
||||
func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return jsonv1.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
|
||||
type jsonV2Encoder struct {
|
||||
writer io.Writer
|
||||
opts jsonv2.Options
|
||||
}
|
||||
|
||||
func (e *jsonV2Encoder) Encode(v any) error {
|
||||
return jsonv2.MarshalWrite(e.writer, v, e.opts)
|
||||
}
|
||||
|
||||
type jsonV2Decoder struct {
|
||||
reader io.Reader
|
||||
opts jsonv2.Options
|
||||
}
|
||||
|
||||
func (d *jsonV2Decoder) Decode(v any) error {
|
||||
return jsonv2.UnmarshalRead(d.reader, v, d.opts)
|
||||
}
|
||||
|
||||
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
|
||||
return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
||||
},
|
||||
{
|
||||
endpoint: "https://invalid-json-response.io",
|
||||
expectedError: "invalid json",
|
||||
expectedError: "/(invalid json|invalid character)/",
|
||||
},
|
||||
{
|
||||
endpoint: "https://valid-batch-request-download.io",
|
||||
@@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
if c.expectedError != "" {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
|
||||
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
|
||||
} else {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
|
||||
},
|
||||
{
|
||||
endpoint: "https://invalid-json-response.io",
|
||||
expectedError: "invalid json",
|
||||
expectedError: "/(invalid json|invalid character)/",
|
||||
},
|
||||
{
|
||||
endpoint: "https://valid-batch-request-upload.io",
|
||||
@@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) {
|
||||
return io.NopCloser(new(bytes.Buffer)), objectError
|
||||
})
|
||||
if c.expectedError != "" {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
|
||||
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
|
||||
} else {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,17 @@ import (
|
||||
)
|
||||
|
||||
type testSerializationStruct struct {
|
||||
NormalString string `json:"normal_string" yaml:"normal_string"`
|
||||
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
|
||||
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
|
||||
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
|
||||
NormalString string `json:"normal_string" yaml:"normal_string"`
|
||||
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
|
||||
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
|
||||
|
||||
// It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string?
|
||||
// The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea.
|
||||
// If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior
|
||||
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
|
||||
|
||||
OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"`
|
||||
OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
|
||||
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
|
||||
}
|
||||
|
||||
func TestOptionalToJson(t *testing.T) {
|
||||
@@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
obj: new(testSerializationStruct),
|
||||
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
|
||||
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`,
|
||||
},
|
||||
{
|
||||
name: "some",
|
||||
@@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) {
|
||||
OptTwoBool: optional.None[bool](),
|
||||
OptTwoString: optional.None[string](),
|
||||
},
|
||||
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
|
||||
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b, err := json.Marshal(tc.obj)
|
||||
b, err := json.MarshalKeepOptionalEmpty(tc.obj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected")
|
||||
|
||||
@@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "some",
|
||||
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
|
||||
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
|
||||
want: testSerializationStruct{
|
||||
NormalString: "a string",
|
||||
NormalBool: true,
|
||||
@@ -169,7 +174,7 @@ normal_bool: true
|
||||
optional_bool: false
|
||||
optional_string: ""
|
||||
optional_two_bool: null
|
||||
optional_twostring: null
|
||||
optional_two_string: null
|
||||
`,
|
||||
want: testSerializationStruct{
|
||||
NormalString: "a string",
|
||||
|
||||
@@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
|
||||
|
||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
||||
var image oci.Image
|
||||
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
||||
// FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive
|
||||
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
|
||||
if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) {
|
||||
repositoryURL := "https://gitea.com/gitea"
|
||||
documentationURL := "https://docs.gitea.com"
|
||||
|
||||
// FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec
|
||||
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
|
||||
configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}`
|
||||
|
||||
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
|
||||
|
||||
@@ -5,6 +5,7 @@ package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
@@ -121,12 +122,12 @@ func (p *DBProvider) Read(sid string) (session.RawStore, error) {
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (p *DBProvider) Exist(sid string) bool {
|
||||
func (p *DBProvider) Exist(sid string) (bool, error) {
|
||||
has, err := auth.ExistSession(dbContext(), sid)
|
||||
if err != nil {
|
||||
panic("session/DB: error checking existence: " + err.Error())
|
||||
return false, fmt.Errorf("session/DB: error checking existence: %w", err)
|
||||
}
|
||||
return has
|
||||
return has, nil
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
@@ -155,12 +156,12 @@ func (p *DBProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err err
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (p *DBProvider) Count() int {
|
||||
func (p *DBProvider) Count() (int, error) {
|
||||
total, err := auth.CountSessions(dbContext())
|
||||
if err != nil {
|
||||
panic("session/DB: error counting records: " + err.Error())
|
||||
return 0, fmt.Errorf("session/DB: error counting records: %w", err)
|
||||
}
|
||||
return int(total)
|
||||
return int(total), nil
|
||||
}
|
||||
|
||||
// GC calls GC to clean expired sessions.
|
||||
|
||||
@@ -135,10 +135,12 @@ func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) {
|
||||
// Read returns raw session store by session ID.
|
||||
func (p *RedisProvider) Read(sid string) (session.RawStore, error) {
|
||||
psid := p.prefix + sid
|
||||
if !p.Exist(sid) {
|
||||
if exist, err := p.Exist(sid); err == nil && !exist {
|
||||
if err := p.c.Set(graceful.GetManager().HammerContext(), psid, "", p.duration).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kv map[any]any
|
||||
@@ -159,9 +161,9 @@ func (p *RedisProvider) Read(sid string) (session.RawStore, error) {
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (p *RedisProvider) Exist(sid string) bool {
|
||||
func (p *RedisProvider) Exist(sid string) (bool, error) {
|
||||
v, err := p.c.Exists(graceful.GetManager().HammerContext(), p.prefix+sid).Result()
|
||||
return err == nil && v == 1
|
||||
return err == nil && v == 1, err
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
@@ -174,13 +176,18 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err
|
||||
poldsid := p.prefix + oldsid
|
||||
psid := p.prefix + sid
|
||||
|
||||
if p.Exist(sid) {
|
||||
if exist, err := p.Exist(sid); err != nil {
|
||||
return nil, err
|
||||
} else if exist {
|
||||
return nil, fmt.Errorf("new sid '%s' already exists", sid)
|
||||
} else if !p.Exist(oldsid) {
|
||||
}
|
||||
if exist, err := p.Exist(oldsid); err == nil && !exist {
|
||||
// Make a fake old session.
|
||||
if err = p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil {
|
||||
if err := p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// do not use Rename here, because the old sid and new sid may be in different redis cluster slot.
|
||||
@@ -211,12 +218,9 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (p *RedisProvider) Count() int {
|
||||
func (p *RedisProvider) Count() (int, error) {
|
||||
size, err := p.c.DBSize(graceful.GetManager().HammerContext()).Result()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int(size)
|
||||
return int(size), err
|
||||
}
|
||||
|
||||
// GC calls GC to clean expired sessions.
|
||||
|
||||
@@ -59,8 +59,10 @@ func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error {
|
||||
func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
|
||||
o.lock.RLock()
|
||||
defer o.lock.RUnlock()
|
||||
if o.provider.Exist(sid) {
|
||||
if exist, err := o.provider.Exist(sid); err == nil && exist {
|
||||
return o.provider.Read(sid)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
|
||||
}
|
||||
kv := make(map[any]any)
|
||||
kv["_old_uid"] = "0"
|
||||
@@ -68,8 +70,8 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
func (o *VirtualSessionProvider) Exist(sid string) bool {
|
||||
return true
|
||||
func (o *VirtualSessionProvider) Exist(sid string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Destroy deletes a session by session ID.
|
||||
@@ -87,7 +89,7 @@ func (o *VirtualSessionProvider) Regenerate(oldsid, sid string) (session.RawStor
|
||||
}
|
||||
|
||||
// Count counts and returns number of sessions.
|
||||
func (o *VirtualSessionProvider) Count() int {
|
||||
func (o *VirtualSessionProvider) Count() (int, error) {
|
||||
o.lock.RLock()
|
||||
defer o.lock.RUnlock()
|
||||
return o.provider.Count()
|
||||
@@ -162,9 +164,13 @@ func (s *VirtualStore) Release() error {
|
||||
// Now ensure that we don't exist!
|
||||
realProvider := s.p.provider
|
||||
|
||||
if !s.released && realProvider.Exist(s.sid) {
|
||||
// This is an error!
|
||||
return fmt.Errorf("new sid '%s' already exists", s.sid)
|
||||
if !s.released {
|
||||
if exist, err := realProvider.Exist(s.sid); err == nil && exist {
|
||||
// This is an error!
|
||||
return fmt.Errorf("new sid '%s' already exists", s.sid)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("check if '%s' exist failed: %w", s.sid, err)
|
||||
}
|
||||
}
|
||||
realStore, err := realProvider.Read(s.sid)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
)
|
||||
|
||||
// StatusTable is a table maintains true/false values.
|
||||
//
|
||||
// This table is particularly useful for un/marking and checking values
|
||||
// in different goroutines.
|
||||
type StatusTable struct {
|
||||
lock sync.RWMutex
|
||||
pool container.Set[string]
|
||||
}
|
||||
|
||||
// NewStatusTable initializes and returns a new StatusTable object.
|
||||
func NewStatusTable() *StatusTable {
|
||||
return &StatusTable{
|
||||
pool: make(container.Set[string]),
|
||||
}
|
||||
}
|
||||
|
||||
// StartIfNotRunning sets value of given name to true if not already in pool.
|
||||
// Returns whether set value was set to true
|
||||
func (p *StatusTable) StartIfNotRunning(name string) bool {
|
||||
p.lock.Lock()
|
||||
added := p.pool.Add(name)
|
||||
p.lock.Unlock()
|
||||
return added
|
||||
}
|
||||
|
||||
// Start sets value of given name to true in the pool.
|
||||
func (p *StatusTable) Start(name string) {
|
||||
p.lock.Lock()
|
||||
p.pool.Add(name)
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// Stop sets value of given name to false in the pool.
|
||||
func (p *StatusTable) Stop(name string) {
|
||||
p.lock.Lock()
|
||||
p.pool.Remove(name)
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// IsRunning checks if value of given name is set to true in the pool.
|
||||
func (p *StatusTable) IsRunning(name string) bool {
|
||||
p.lock.RLock()
|
||||
exists := p.pool.Contains(name)
|
||||
p.lock.RUnlock()
|
||||
return exists
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_StatusTable(t *testing.T) {
|
||||
table := NewStatusTable()
|
||||
|
||||
assert.False(t, table.IsRunning("xyz"))
|
||||
|
||||
table.Start("xyz")
|
||||
assert.True(t, table.IsRunning("xyz"))
|
||||
|
||||
assert.False(t, table.StartIfNotRunning("xyz"))
|
||||
assert.True(t, table.IsRunning("xyz"))
|
||||
|
||||
table.Stop("xyz")
|
||||
assert.False(t, table.IsRunning("xyz"))
|
||||
|
||||
assert.True(t, table.StartIfNotRunning("xyz"))
|
||||
assert.True(t, table.IsRunning("xyz"))
|
||||
|
||||
table.Stop("xyz")
|
||||
assert.False(t, table.IsRunning("xyz"))
|
||||
}
|
||||
@@ -44,9 +44,11 @@ func isRoutePathExpensive(routePattern string) bool {
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/compare/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/rss/branch/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
|
||||
@@ -107,8 +107,8 @@ func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Han
|
||||
return proxy.ForwardedHeaders(opt)
|
||||
}
|
||||
|
||||
func Sessioner() func(next http.Handler) http.Handler {
|
||||
return session.Sessioner(session.Options{
|
||||
func Sessioner() (func(next http.Handler) http.Handler, error) {
|
||||
middleware, err := session.Sessioner(session.Options{
|
||||
Provider: setting.SessionConfig.Provider,
|
||||
ProviderConfig: setting.SessionConfig.ProviderConfig,
|
||||
CookieName: setting.SessionConfig.CookieName,
|
||||
@@ -119,4 +119,9 @@ func Sessioner() func(next http.Handler) http.Handler {
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
Domain: setting.SessionConfig.Domain,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session middleware: %w", err)
|
||||
}
|
||||
|
||||
return middleware, nil
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func InitWebInstalled(ctx context.Context) {
|
||||
mustInit(repo_service.InitLicenseClassifier)
|
||||
|
||||
// Finally start up the cron
|
||||
cron.NewContext(ctx)
|
||||
cron.Init(ctx)
|
||||
}
|
||||
|
||||
// NormalRoutes represents non install routes
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"html"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -23,7 +24,11 @@ func Routes() *web.Router {
|
||||
base.Methods("GET, HEAD", "/assets/*", public.FileHandlerFunc())
|
||||
|
||||
r := web.NewRouter()
|
||||
r.Use(common.Sessioner(), Contexter())
|
||||
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
|
||||
r.Use(sessionMid, Contexter())
|
||||
} else {
|
||||
log.Fatal("common.Sessioner failed: %v", err)
|
||||
}
|
||||
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
|
||||
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
|
||||
r.Get("/post-install", InstallDone)
|
||||
|
||||
@@ -267,7 +267,11 @@ func Routes() *web.Router {
|
||||
routes.Get("/ssh_info", misc.SSHInfo)
|
||||
routes.Get("/api/healthz", healthcheck.Check)
|
||||
|
||||
mid = append(mid, common.Sessioner(), context.Contexter())
|
||||
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
|
||||
mid = append(mid, sessionMid, context.Contexter())
|
||||
} else {
|
||||
log.Fatal("common.Sessioner failed: %v", err)
|
||||
}
|
||||
|
||||
// Get user from session if logged in.
|
||||
mid = append(mid, webAuth(buildAuthGroup()))
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/sync"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"github.com/go-co-op/gocron"
|
||||
@@ -19,13 +18,10 @@ import (
|
||||
|
||||
var scheduler = gocron.NewScheduler(time.Local)
|
||||
|
||||
// Prevent duplicate running tasks.
|
||||
var taskStatusTable = sync.NewStatusTable()
|
||||
|
||||
// NewContext begins cron tasks
|
||||
// Init begins cron tasks
|
||||
// Each cron task is run within the shutdown context as a running server
|
||||
// AtShutdown the cron server is stopped
|
||||
func NewContext(original context.Context) {
|
||||
func Init(original context.Context) {
|
||||
defer pprof.SetGoroutineLabels(original)
|
||||
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().ShutdownContext(), "Service: Cron", process.SystemProcessType, true)
|
||||
initBasicTasks()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
@@ -71,20 +72,30 @@ func (t *Task) Run() {
|
||||
}, t.config)
|
||||
}
|
||||
|
||||
func getCronTaskLockKey(name string) string {
|
||||
return "cron_task:" + name
|
||||
}
|
||||
|
||||
// RunWithUser will run the task incrementing the cron counter at the time with User
|
||||
func (t *Task) RunWithUser(doer *user_model.User, config Config) {
|
||||
if !taskStatusTable.StartIfNotRunning(t.Name) {
|
||||
locked, releaser, err := globallock.TryLock(graceful.GetManager().ShutdownContext(), getCronTaskLockKey(t.Name))
|
||||
if err != nil {
|
||||
log.Error("Failed to acquire lock for cron task %q: %v", t.Name, err)
|
||||
return
|
||||
}
|
||||
if !locked {
|
||||
log.Trace("a cron task %q is already running", t.Name)
|
||||
return
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
t.lock.Lock()
|
||||
if config == nil {
|
||||
config = t.config
|
||||
}
|
||||
t.ExecTimes++
|
||||
t.lock.Unlock()
|
||||
defer func() {
|
||||
taskStatusTable.Stop(t.Name)
|
||||
}()
|
||||
|
||||
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
@@ -131,24 +131,74 @@ func TestWebhookDeliverHookTask(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
version2Body := `{
|
||||
"body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1",
|
||||
"msgtype": "",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1",
|
||||
"io.gitea.commits": [
|
||||
{
|
||||
"id": "2020558fe2e34debb818a514715839cabd25e778",
|
||||
"message": "commit message",
|
||||
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
|
||||
"author": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"committer": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"verification": null,
|
||||
"timestamp": "0001-01-01T00:00:00Z",
|
||||
"added": null,
|
||||
"removed": null,
|
||||
"modified": null
|
||||
},
|
||||
{
|
||||
"id": "2020558fe2e34debb818a514715839cabd25e778",
|
||||
"message": "commit message",
|
||||
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
|
||||
"author": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"committer": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"verification": null,
|
||||
"timestamp": "0001-01-01T00:00:00Z",
|
||||
"added": null,
|
||||
"removed": null,
|
||||
"modified": null
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
testVersion := 0
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
switch r.URL.Path {
|
||||
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
|
||||
// Version 1
|
||||
assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/"))
|
||||
assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash
|
||||
switch testVersion {
|
||||
case 1: // Version 1
|
||||
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
|
||||
assert.Empty(t, r.Header.Get("Content-Type"))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{"data": 42}`, string(body))
|
||||
|
||||
case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
|
||||
// Version 2
|
||||
case 2: // Version 2
|
||||
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, body, 2147)
|
||||
assert.JSONEq(t, version2Body, string(body))
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
@@ -172,6 +222,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
|
||||
assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook))
|
||||
|
||||
t.Run("Version 1", func(t *testing.T) {
|
||||
testVersion = 1
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
@@ -198,6 +249,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
|
||||
data, err := p.JSONPayload()
|
||||
assert.NoError(t, err)
|
||||
|
||||
testVersion = 2
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
|
||||
@@ -274,6 +274,7 @@ func getMessageBody(htmlText string) string {
|
||||
|
||||
// getMatrixTxnID computes the transaction ID to ensure idempotency
|
||||
func getMatrixTxnID(payload []byte) (string, error) {
|
||||
payload = bytes.TrimSpace(payload)
|
||||
if len(payload) >= matrixPayloadSizeLimit {
|
||||
return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
@@ -216,7 +217,9 @@ func TestMatrixJSONPayload(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
|
||||
txnID, ok := strings.CutPrefix(req.URL.Path, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/")
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, txnID, 40) // txnID is just a unique ID for a webhook request, it is a sha1 hash from the payload
|
||||
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
var body MatrixPayload
|
||||
|
||||
@@ -318,7 +318,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
AddBasicAuth(user.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
assert.JSONEq(t, body, resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("PackageVersionMetadata", func(t *testing.T) {
|
||||
|
||||
@@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusForbidden)
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
|
||||
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
|
||||
|
||||
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
|
||||
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
|
||||
}
|
||||
|
||||
@@ -413,7 +413,8 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
|
||||
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
|
||||
t.Helper()
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
|
||||
decoder := json.NewDecoderCaseInsensitive(resp.Body)
|
||||
require.NoError(t, decoder.Decode(v))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user