Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c5f251b
use experimental go json v2 library
Aug 31, 2025
cb16b33
Merge branch 'main' into jsonv2
techknowlogick Sep 1, 2025
abc734c
Update go.mod
techknowlogick Sep 2, 2025
f9c8805
Update jsonv2.go
techknowlogick Sep 2, 2025
9d83440
Update jsonv2.go
techknowlogick Sep 2, 2025
31492c1
Update jsonv2.go
techknowlogick Sep 2, 2025
7b0832c
Update jsonv2.go
techknowlogick Sep 2, 2025
fe51938
Update jsonv2_fallback.go
techknowlogick Sep 2, 2025
29d1770
Update jsonv2_fallback.go
techknowlogick Sep 2, 2025
3c3f77f
Update jsonv2_fallback.go
techknowlogick Sep 2, 2025
ce4107d
Merge branch 'main' into jsonv2
techknowlogick Sep 2, 2025
b8fb94d
Update jsonv2_fallback.go
techknowlogick Sep 3, 2025
c748ca8
Update jsonv2_fallback.go
techknowlogick Sep 3, 2025
f9d91f2
Merge branch 'main' into jsonv2
techknowlogick Sep 4, 2025
7fec838
Update Go version from 1.24.6 to 1.25.0
techknowlogick Sep 4, 2025
3c6d690
bump go.mod
techknowlogick Sep 4, 2025
b3969be
use fixed go-swagger version
techknowlogick Sep 4, 2025
ba43047
Merge branch 'main' into jsonv2
techknowlogick Sep 4, 2025
94d537c
Update GOEXPERIMENT to use jsonv2 by default
techknowlogick Sep 4, 2025
82b2d97
fix lint
techknowlogick Sep 4, 2025
502b4fb
clean up modernizer fixes
techknowlogick Sep 4, 2025
d4b1e10
try to fix lint
techknowlogick Sep 4, 2025
8f3b218
try to use go1.25 waitgroup logic
techknowlogick Sep 4, 2025
34af20c
fixup test fails
techknowlogick Sep 4, 2025
a65bff4
Merge remote-tracking branch 'upstream/main' into jsonv2
Sep 10, 2025
cdcb9bd
adjust jsonv2 output to become similar to v1
Sep 10, 2025
ecc304a
resolve vagrant test failure
Sep 11, 2025
748b590
the security check has a panic when using the experimental go library
Sep 11, 2025
60b26b0
resolve panic in assetfs parsing of embeded data
Sep 11, 2025
28beca0
Merge branch 'main' into jsonv2
wxiaoguang Sep 26, 2025
07afc37
fix
wxiaoguang Sep 26, 2025
cb47618
fix
wxiaoguang Sep 26, 2025
f302ca8
fix
wxiaoguang Sep 26, 2025
ad93826
fine tune
wxiaoguang Sep 27, 2025
7f23bb9
Merge branch 'main' into jsonv2
wxiaoguang Sep 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/pull-db-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi we do not use submake, so this export is useless.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it is "useless"? Will go ... inherit non-exported variables?

Copy link
Member

@silverwind silverwind Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all commands will see the variables like FOO ?= bar, export in a Makefile is only for sub-make:

https://www.gnu.org/software/make/manual/html_node/Variables_002fRecursion.html#Communicating-Variables-to-a-Sub_002dmake

Copy link
Contributor

@wxiaoguang wxiaoguang Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure or have you tested? I don't think the the GNU toolchain can have such a counterintuitive, global-polluting and dirty design.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you are right. My test was incorrect. I have always used the explicit form VAR=$(VAR) cmd so far, which explicitly passes variables. I think it's the most clean way, especially because the Makefile handles a lot of other commands that don't accept GOEXPERIMENT.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that "export" is not the best approach, while it just works, so I just kept the original change from techknowlogick's 94d537c


GO ?= go
SHASUM ?= shasum -a 256
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
Expand Down Expand Up @@ -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))),)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 2 additions & 2 deletions modules/assetfs/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 1 addition & 2 deletions modules/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type Interface interface {
}

var (
// DefaultJSONHandler default json handler
DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
DefaultJSONHandler = getDefaultJSONHandler()

_ Interface = StdJSON{}
_ Interface = JSONiter{}
Expand Down
10 changes: 10 additions & 0 deletions modules/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package json

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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())
}
24 changes: 24 additions & 0 deletions modules/json/jsonlegacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

//go:build !goexperiment.jsonv2

package json

import (
"io"

jsoniter "github.com/json-iterator/go"
)

func getDefaultJSONHandler() Interface {
return JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
}

func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
return DefaultJSONHandler.Marshal(v)
}

func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
return DefaultJSONHandler.NewDecoder(reader)
}
92 changes: 92 additions & 0 deletions modules/json/jsonv2.go
Original file line number Diff line number Diff line change
@@ -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}
}
16 changes: 12 additions & 4 deletions modules/lfs/http_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
},
{
endpoint: "https://invalid-json-response.io",
expectedError: "invalid json",
expectedError: "/(invalid json|jsontext: invalid character)/",
},
{
endpoint: "https://valid-batch-request-download.io",
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
},
{
endpoint: "https://invalid-json-response.io",
expectedError: "invalid json",
expectedError: "/(invalid json|jsontext: invalid character)/",
},
{
endpoint: "https://valid-batch-request-upload.io",
Expand Down Expand Up @@ -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)
}
Expand Down
25 changes: 15 additions & 10 deletions modules/optional/serialization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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",
Expand All @@ -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")

Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion modules/packages/container/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions modules/packages/container/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading