diff --git a/go.mod b/go.mod index df57995445..4efce828a1 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( code.gitea.io/sdk/gitea v0.24.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 connectrpc.com/connect v1.19.1 - gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed + gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a 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-20251124165456-68e0254e989e @@ -57,7 +57,6 @@ require ( github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-sql-driver/mysql v1.9.3 github.com/go-webauthn/webauthn v0.16.4 - github.com/goccy/go-json v0.10.6 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.1 @@ -196,6 +195,7 @@ require ( github.com/go-ini/ini v1.67.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-webauthn/x v0.2.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect diff --git a/go.sum b/go.sum index 448606caeb..2392b4ab3f 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs= gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ= 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= diff --git a/modules/json/jsongoccy.go b/modules/json/jsongoccy.go deleted file mode 100644 index 77ea047fa7..0000000000 --- a/modules/json/jsongoccy.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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) -} diff --git a/modules/json/jsonlegacy.go b/modules/json/jsonlegacy.go index 99875b96f1..81d644d4f4 100644 --- a/modules/json/jsonlegacy.go +++ b/modules/json/jsonlegacy.go @@ -11,7 +11,7 @@ import ( ) func getDefaultJSONHandler() Interface { - return jsonGoccy{} + return jsonV1{} } func MarshalKeepOptionalEmpty(v any) ([]byte, error) { diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 86364e1173..1a830ed2eb 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -5,12 +5,14 @@ package validation import ( "fmt" + "io" "regexp" "strings" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/glob" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/util" "gitea.com/go-chi/binding" @@ -31,8 +33,23 @@ const ( ErrInvalidBadgeSlug = "InvalidBadgeSlug" ) +type jsonProvider struct{} + +func (j jsonProvider) Marshal(v any) ([]byte, error) { return json.Marshal(v) } + +func (j jsonProvider) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) } + +func (j jsonProvider) NewDecoder(reader io.Reader) binding.JSONDecoder { + return json.NewDecoder(reader) +} + +func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder { + return json.NewEncoder(writer) +} + // AddBindingRules adds additional binding rules func AddBindingRules() { + binding.JSONProvider = jsonProvider{} addGitRefNameBindingRule() addValidURLListBindingRule() addValidURLBindingRule() diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 3792190a76..01e57a596e 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -10,7 +10,9 @@ import ( issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/webhook" @@ -523,16 +525,49 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) type MergePullRequestForm struct { // required: true // enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"] - Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` - MergeTitleField string - MergeMessageField string - MergeCommitID string // only used for manually-merged + Do string `json:"do" binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` + MergeTitleField string `json:"merge_title_field,omitempty"` + MergeMessageField string `json:"merge_message_field,omitempty"` + MergeCommitID string `json:"merge_commit_id,omitempty"` // only used for manually-merged HeadCommitID string `json:"head_commit_id,omitempty"` ForceMerge bool `json:"force_merge,omitempty"` MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"` DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"` } +func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error { + // This is for backward compatibility, to support both field names like "do" and "Do", + // because old code doesn't have "json" tag for these fields + type aux struct { + Do1 string `json:"do"` + Do2 string `json:"Do"` + MergeTitleField1 string `json:"merge_title_field"` + MergeTitleField2 string `json:"MergeTitleField"` + MergeMessageField1 string `json:"merge_message_field"` + MergeMessageField2 string `json:"MergeMessageField"` + MergeCommitID1 string `json:"merge_commit_id"` + MergeCommitID2 string `json:"MergeCommitID"` + + HeadCommitID string `json:"head_commit_id"` + ForceMerge bool `json:"force_merge"` + MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed"` + DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"` + } + var a aux + if err := json.Unmarshal(b, &a); err != nil { + return err + } + f.Do = util.IfZero(a.Do1, a.Do2) + f.MergeTitleField = util.IfZero(a.MergeTitleField1, a.MergeTitleField2) + f.MergeMessageField = util.IfZero(a.MergeMessageField1, a.MergeMessageField2) + f.MergeCommitID = util.IfZero(a.MergeCommitID1, a.MergeCommitID2) + f.HeadCommitID = a.HeadCommitID + f.ForceMerge = a.ForceMerge + f.MergeWhenChecksSucceed = a.MergeWhenChecksSucceed + f.DeleteBranchAfterMerge = a.DeleteBranchAfterMerge + return nil +} + // Validate validates the fields func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { ctx := context.GetValidateContext(req) diff --git a/services/forms/repo_form_test.go b/services/forms/repo_form_test.go index a0c67fe0f8..6f2c966dde 100644 --- a/services/forms/repo_form_test.go +++ b/services/forms/repo_form_test.go @@ -6,7 +6,10 @@ package forms import ( "testing" + "code.gitea.io/gitea/modules/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSubmitReviewForm_IsEmpty(t *testing.T) { @@ -37,3 +40,48 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) { assert.Equal(t, v.expected, v.form.HasEmptyContent()) } } + +func TestMergePullRequestForm(t *testing.T) { + expected := &MergePullRequestForm{ + Do: "merge", + MergeTitleField: "title", + MergeMessageField: "message", + MergeCommitID: "merge-id", + HeadCommitID: "head-id", + ForceMerge: true, + MergeWhenChecksSucceed: true, + DeleteBranchAfterMerge: new(true), + } + + t.Run("NewFields", func(t *testing.T) { + input := `{ + "do": "merge", + "merge_title_field": "title", + "merge_message_field": "message", + "merge_commit_id": "merge-id", + "head_commit_id": "head-id", + "force_merge": true, + "merge_when_checks_succeed": true, + "delete_branch_after_merge": true +}` + var m *MergePullRequestForm + require.NoError(t, json.Unmarshal([]byte(input), &m)) + assert.Equal(t, expected, m) + }) + + t.Run("OldFields", func(t *testing.T) { + input := `{ + "Do": "merge", + "MergeTitleField": "title", + "MergeMessageField": "message", + "MergeCommitID": "merge-id", + "head_commit_id": "head-id", + "force_merge": true, + "merge_when_checks_succeed": true, + "delete_branch_after_merge": true +}` + var m *MergePullRequestForm + require.NoError(t, json.Unmarshal([]byte(input), &m)) + assert.Equal(t, expected, m) + }) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 703a25336f..48a40eae08 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -26804,10 +26804,14 @@ "description": "MergePullRequestForm form for merging Pull Request", "type": "object", "required": [ - "Do" + "do" ], "properties": { - "Do": { + "delete_branch_after_merge": { + "type": "boolean", + "x-go-name": "DeleteBranchAfterMerge" + }, + "do": { "type": "string", "enum": [ "merge", @@ -26816,20 +26820,8 @@ "squash", "fast-forward-only", "manually-merged" - ] - }, - "MergeCommitID": { - "type": "string" - }, - "MergeMessageField": { - "type": "string" - }, - "MergeTitleField": { - "type": "string" - }, - "delete_branch_after_merge": { - "type": "boolean", - "x-go-name": "DeleteBranchAfterMerge" + ], + "x-go-name": "Do" }, "force_merge": { "type": "boolean", @@ -26839,6 +26831,18 @@ "type": "string", "x-go-name": "HeadCommitID" }, + "merge_commit_id": { + "type": "string", + "x-go-name": "MergeCommitID" + }, + "merge_message_field": { + "type": "string", + "x-go-name": "MergeMessageField" + }, + "merge_title_field": { + "type": "string", + "x-go-name": "MergeTitleField" + }, "merge_when_checks_succeed": { "type": "boolean", "x-go-name": "MergeWhenChecksSucceed" diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go index d099e72edb..88038799e5 100644 --- a/tests/integration/api_issue_reaction_test.go +++ b/tests/integration/api_issue_reaction_test.go @@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) { DecodeJSON(t, resp, &apiNewReaction) // Add existing reaction - MakeRequest(t, req, http.StatusForbidden) + req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ + Reaction: "rocket", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) // Blocked user can't react to comment user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) @@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) { DecodeJSON(t, resp, &apiNewReaction) // Add existing reaction - MakeRequest(t, req, http.StatusForbidden) + req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ + Reaction: "+1", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) // Get end result of reaction list of issue #1 req = NewRequest(t, "GET", urlStr).