From f3477681fe2afc2ad2a60da7cead9f4c8a4ae4d2 Mon Sep 17 00:00:00 2001 From: Surya Purohit Date: Thu, 9 Oct 2025 13:53:22 +0530 Subject: [PATCH 1/6] fix(swagger): Update diffpatch endpoint to use ApplyDiffPatchOptions --- routers/api/v1/repo/patch.go | 4 +- routers/api/v1/swagger/options.go | 3 + templates/swagger/v1_json.tmpl | 150 ++++++++++++++++++++---------- 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index e9f5cf5d908da..c71bb514414d1 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -14,7 +14,7 @@ import ( // ApplyDiffPatch handles API call for applying a patch func ApplyDiffPatch(ctx *context.APIContext) { - // swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch + // swagger:operation POST /repos/{owner}/{repo}/contents/diffpatch repository repoApplyDiffPatch // --- // summary: Apply diff patch to repository // consumes: @@ -36,7 +36,7 @@ func ApplyDiffPatch(ctx *context.APIContext) { // in: body // required: true // schema: - // "$ref": "#/definitions/UpdateFileOptions" + // "$ref": "#/definitions/ApplyDiffPatchFileOptions" // responses: // "200": // "$ref": "#/responses/FileResponse" diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 4aba74b939613..8f77bb2d805d2 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -222,4 +222,7 @@ type swaggerParameterBodies struct { // in:body LockIssueOption api.LockIssueOption + + // in:body + ApplyDiffPatchFileOptions api.ApplyDiffPatchFileOptions } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0df8356fd9c38..bd9902a9f4758 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7575,6 +7575,56 @@ } } }, + "/repos/{owner}/{repo}/contents/diffpatch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Apply diff patch to repository", + "operationId": "repoApplyDiffPatch", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApplyDiffPatchFileOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/FileResponse" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/{owner}/{repo}/contents/{filepath}": { "get": { "description": "This API follows GitHub's design, and it is not easy to use. Recommend users to use the \"contents-ext\" API instead.", @@ -7811,56 +7861,6 @@ } } }, - "/repos/{owner}/{repo}/diffpatch": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "repository" - ], - "summary": "Apply diff patch to repository", - "operationId": "repoApplyDiffPatch", - "parameters": [ - { - "type": "string", - "description": "owner of the repo", - "name": "owner", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name of the repo", - "name": "repo", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/UpdateFileOptions" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/FileResponse" - }, - "404": { - "$ref": "#/responses/notFound" - }, - "423": { - "$ref": "#/responses/repoArchivedError" - } - } - } - }, "/repos/{owner}/{repo}/editorconfig/{filepath}": { "get": { "produces": [ @@ -21645,6 +21645,54 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ApplyDiffPatchFileOptions": { + "description": "ApplyDiffPatchFileOptions options for applying a diff patch\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", + "type": "object", + "required": [ + "content" + ], + "properties": { + "author": { + "$ref": "#/definitions/Identity" + }, + "branch": { + "description": "branch (optional) is the base branch for the changes. If not supplied, the default branch is used", + "type": "string", + "x-go-name": "BranchName" + }, + "committer": { + "$ref": "#/definitions/Identity" + }, + "content": { + "type": "string", + "x-go-name": "Content" + }, + "dates": { + "$ref": "#/definitions/CommitDateOptions" + }, + "force_push": { + "description": "force_push (optional) will do a force-push if the new branch already exists", + "type": "boolean", + "x-go-name": "ForcePush" + }, + "message": { + "description": "message (optional) is the commit message of the changes. If not supplied, a default message will be used", + "type": "string", + "x-go-name": "Message" + }, + "new_branch": { + "description": "new_branch (optional) will make a new branch from base branch for the changes. If not supplied, the changes will be committed to the base branch", + "type": "string", + "x-go-name": "NewBranchName" + }, + "signoff": { + "description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.", + "type": "boolean", + "x-go-name": "Signoff" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Attachment": { "description": "Attachment a generic attachment", "type": "object", @@ -30249,7 +30297,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/LockIssueOption" + "$ref": "#/definitions/ApplyDiffPatchFileOptions" } }, "redirect": { From fbbb1baf50a7ee7b483d98ba5d5ffa980b454c03 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Oct 2025 17:15:22 +0800 Subject: [PATCH 2/6] fix --- routers/api/v1/api.go | 2 +- routers/api/v1/repo/patch.go | 2 +- .../api_repo_file_diffpatch_test.go | 89 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/integration/api_repo_file_diffpatch_test.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 66afede218c65..c04dfb15a9578 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1423,6 +1423,7 @@ func Routes() *web.Router { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) + m.Post("/diffpatch", mustEnableEditor, reqToken(), context.ReferencesGitRepo(), bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) m.Get("/*", repo.GetContents) @@ -1434,7 +1435,6 @@ func Routes() *web.Router { m.Put("", bind(api.UpdateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.UpdateFile) m.Delete("", bind(api.DeleteFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.DeleteFile) }) - m.Post("/diffpatch", bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) }, mustEnableEditor, reqToken()) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) m.Group("/contents-ext", func() { diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index c71bb514414d1..edb09fc08fa08 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -14,7 +14,7 @@ import ( // ApplyDiffPatch handles API call for applying a patch func ApplyDiffPatch(ctx *context.APIContext) { - // swagger:operation POST /repos/{owner}/{repo}/contents/diffpatch repository repoApplyDiffPatch + // swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch // --- // summary: Apply diff patch to repository // consumes: diff --git a/tests/integration/api_repo_file_diffpatch_test.go b/tests/integration/api_repo_file_diffpatch_test.go new file mode 100644 index 0000000000000..c93987b3446e1 --- /dev/null +++ b/tests/integration/api_repo_file_diffpatch_test.go @@ -0,0 +1,89 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func getApplyDiffPatchFileOptions() *api.ApplyDiffPatchFileOptions { + return &api.ApplyDiffPatchFileOptions{ + FileOptions: api.FileOptions{ + BranchName: "master", + }, + Content: `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++File 1 +`, + } +} + +func TestAPIApplyDiffPatchFileOptions(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo + repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo + + session2 := loginUser(t, user2.Name) + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + session4 := loginUser(t, user4.Name) + token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/diffpatch", getApplyDiffPatchFileOptions()).AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusCreated) + var fileResponse api.FileResponse + DecodeJSON(t, resp, &fileResponse) + assert.Nil(t, fileResponse.Content) + assert.NotEmpty(t, fileResponse.Commit.HTMLURL) + req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/raw/patch-file-1.txt") + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "File 1\n", string(resp.Body.Bytes())) + + // Test creating a file in repo1 by user4 who does not have write access + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token4) + MakeRequest(t, req, http.StatusNotFound) + + // Tests a repo with no token given so will fail + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()) + MakeRequest(t, req, http.StatusNotFound) + + // Test using access token for a private repo that the user of the token owns + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + // Test using org repo "org3/repo3" where user2 is a collaborator + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", org3.Name, repo3.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + // Test using org repo "org3/repo3" with no user token + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", org3.Name, repo3.Name), getApplyDiffPatchFileOptions()) + MakeRequest(t, req, http.StatusNotFound) + + // Test using repo "user2/repo1" where user4 is a NOT collaborator + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo1.Name), getApplyDiffPatchFileOptions()). + AddTokenAuth(token4) + MakeRequest(t, req, http.StatusForbidden) + }) +} From 983abda2763f4964b6daaf2858f461f38ee43f11 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Oct 2025 17:24:34 +0800 Subject: [PATCH 3/6] no ReferencesGitRepo --- routers/api/v1/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c04dfb15a9578..5f52ee8a4323d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1423,7 +1423,7 @@ func Routes() *web.Router { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) - m.Post("/diffpatch", mustEnableEditor, reqToken(), context.ReferencesGitRepo(), bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) + m.Post("/diffpatch", mustEnableEditor, reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) m.Get("/*", repo.GetContents) From dd76dac5f54b14e139d04d14297b8b83a880c403 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Oct 2025 17:34:53 +0800 Subject: [PATCH 4/6] sync swagger --- templates/swagger/v1_json.tmpl | 100 ++++++++++++++++----------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index bd9902a9f4758..505f7adeeb8d3 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7575,56 +7575,6 @@ } } }, - "/repos/{owner}/{repo}/contents/diffpatch": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "repository" - ], - "summary": "Apply diff patch to repository", - "operationId": "repoApplyDiffPatch", - "parameters": [ - { - "type": "string", - "description": "owner of the repo", - "name": "owner", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name of the repo", - "name": "repo", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/ApplyDiffPatchFileOptions" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/FileResponse" - }, - "404": { - "$ref": "#/responses/notFound" - }, - "423": { - "$ref": "#/responses/repoArchivedError" - } - } - } - }, "/repos/{owner}/{repo}/contents/{filepath}": { "get": { "description": "This API follows GitHub's design, and it is not easy to use. Recommend users to use the \"contents-ext\" API instead.", @@ -7861,6 +7811,56 @@ } } }, + "/repos/{owner}/{repo}/diffpatch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Apply diff patch to repository", + "operationId": "repoApplyDiffPatch", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApplyDiffPatchFileOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/FileResponse" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/{owner}/{repo}/editorconfig/{filepath}": { "get": { "produces": [ From 12bd9c96e717052d4540fbe8df71f6f8362de8a6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Oct 2025 17:38:38 +0800 Subject: [PATCH 5/6] avoid unnecessary swagger change --- routers/api/v1/swagger/options.go | 6 +++--- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 8f77bb2d805d2..b80a9c14ba027 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -121,6 +121,9 @@ type swaggerParameterBodies struct { // in:body GetFilesOptions api.GetFilesOptions + // in:body + ApplyDiffPatchFileOptions api.ApplyDiffPatchFileOptions + // in:body ChangeFilesOptions api.ChangeFilesOptions @@ -222,7 +225,4 @@ type swaggerParameterBodies struct { // in:body LockIssueOption api.LockIssueOption - - // in:body - ApplyDiffPatchFileOptions api.ApplyDiffPatchFileOptions } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 505f7adeeb8d3..325f0b78a0564 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -30297,7 +30297,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/ApplyDiffPatchFileOptions" + "$ref": "#/definitions/LockIssueOption" } }, "redirect": { From 534f32ac7546bf6e15f6849123d9c0978289ae14 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 9 Oct 2025 17:58:22 +0800 Subject: [PATCH 6/6] fix lint --- tests/integration/api_repo_file_diffpatch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/api_repo_file_diffpatch_test.go b/tests/integration/api_repo_file_diffpatch_test.go index c93987b3446e1..e463027ed3c83 100644 --- a/tests/integration/api_repo_file_diffpatch_test.go +++ b/tests/integration/api_repo_file_diffpatch_test.go @@ -56,7 +56,7 @@ func TestAPIApplyDiffPatchFileOptions(t *testing.T) { assert.NotEmpty(t, fileResponse.Commit.HTMLURL) req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/raw/patch-file-1.txt") resp = MakeRequest(t, req, http.StatusOK) - assert.Equal(t, "File 1\n", string(resp.Body.Bytes())) + assert.Equal(t, "File 1\n", resp.Body.String()) // Test creating a file in repo1 by user4 who does not have write access req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/diffpatch", user2.Name, repo16.Name), getApplyDiffPatchFileOptions()).