From 07e8684a6132f50b195eebe8d097cfad25a7b72a Mon Sep 17 00:00:00 2001 From: oliverpool Date: Fri, 13 Jun 2025 11:26:59 +0200 Subject: [PATCH 1/2] api: GitBlob consistent naming --- modules/structs/git_blob.go | 4 ++-- routers/api/v1/repo/blob.go | 4 ++-- routers/api/v1/swagger/repo.go | 8 ++++---- services/repository/files/content.go | 6 +++--- services/repository/files/content_test.go | 2 +- tests/integration/api_repo_git_blobs_test.go | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/structs/git_blob.go b/modules/structs/git_blob.go index 96c7a271a9..ef06693905 100644 --- a/modules/structs/git_blob.go +++ b/modules/structs/git_blob.go @@ -3,8 +3,8 @@ package structs -// GitBlobResponse represents a git blob -type GitBlobResponse struct { +// GitBlob represents a git blob +type GitBlob struct { Content string `json:"content"` Encoding string `json:"encoding"` URL string `json:"url"` diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index 8ed57d4787..ebff5aba26 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -30,12 +30,12 @@ func GetBlob(ctx *context.APIContext) { // required: true // - name: sha // in: path - // description: sha of the commit + // description: sha of the blob to retrieve // type: string // required: true // responses: // "200": - // "$ref": "#/responses/GitBlobResponse" + // "$ref": "#/responses/GitBlob" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index a27e94253b..e69eeea639 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -231,11 +231,11 @@ type swaggerGitTreeResponse struct { Body api.GitTreeResponse `json:"body"` } -// GitBlobResponse -// swagger:response GitBlobResponse -type swaggerGitBlobResponse struct { +// GitBlob +// swagger:response GitBlob +type swaggerGitBlob struct { // in: body - Body api.GitBlobResponse `json:"body"` + Body api.GitBlob `json:"body"` } // Commit diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 5f7dd38303..d51e5a4361 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -250,8 +250,8 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref return contentsResponse, nil } -// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash. -func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { +// GetBlobBySHA get the GitBlob of a repository using a sha hash. +func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlob, error) { gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, err @@ -263,7 +263,7 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git return nil, err } } - return &api.GitBlobResponse{ + return &api.GitBlob{ SHA: gitBlob.ID.String(), URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()), Size: gitBlob.Size(), diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index e55b840660..70b79bb58d 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -192,7 +192,7 @@ func TestGetBlobBySHA(t *testing.T) { defer gitRepo.Close() gbr, err := GetBlobBySHA(db.DefaultContext, repo, gitRepo, "65f1bf27bc3bf70f64657658635e66094edbcb4d") - expectedGBR := &api.GitBlobResponse{ + expectedGBR := &api.GitBlob{ Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK", Encoding: "base64", URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go index 980fff1e52..a4424a3348 100644 --- a/tests/integration/api_repo_git_blobs_test.go +++ b/tests/integration/api_repo_git_blobs_test.go @@ -37,7 +37,7 @@ func TestAPIReposGitBlobs(t *testing.T) { // Test a public repo that anyone can GET the blob of req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA) resp := MakeRequest(t, req, http.StatusOK) - var gitBlobResponse api.GitBlobResponse + var gitBlobResponse api.GitBlob DecodeJSON(t, resp, &gitBlobResponse) assert.NotNil(t, gitBlobResponse) expectedContent := "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK" From a4ea74020f77aadc50f1763ec932f208753c493d Mon Sep 17 00:00:00 2001 From: oliverpool Date: Fri, 13 Jun 2025 11:36:57 +0200 Subject: [PATCH 2/2] feat: API GET /repos/{owner}/{repo}/git/blobs --- routers/api/v1/api.go | 1 + routers/api/v1/repo/blob.go | 43 +++++++++++++++ routers/api/v1/swagger/repo.go | 7 +++ services/repository/files/content.go | 17 ++++++ services/repository/files/content_test.go | 40 ++++++++++++++ templates/swagger/v1_json.tmpl | 66 ++++++++++++++++++++--- 6 files changed, 167 insertions(+), 7 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e371ebb28b..bf08bdd249 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1310,6 +1310,7 @@ func Routes() *web.Route { m.Get("/refs", repo.GetGitAllRefs) m.Get("/refs/*", repo.GetGitRefs) m.Get("/trees/{sha}", repo.GetTree) + m.Get("/blobs", repo.GetBlobs) m.Get("/blobs/{sha}", repo.GetBlob) m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Group("/notes/{sha}", func() { diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index ebff5aba26..63baec2025 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -5,11 +5,54 @@ package repo import ( "net/http" + "strings" "forgejo.org/services/context" files_service "forgejo.org/services/repository/files" ) +// GetBlobs gets multiple blobs of a repository. +func GetBlobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/blobs repository GetBlobs + // --- + // summary: Gets multiplbe blobs of a repository. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: shas + // in: query + // description: a comma separated list of blob-sha (mind the overall URL-length limit of ~2,083 chars) + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/GitBlobList" + // "400": + // "$ref": "#/responses/error" + + shas := ctx.FormString("shas") + if len(shas) == 0 { + ctx.Error(http.StatusBadRequest, "", "shas not provided") + return + } + + if blobs, err := files_service.GetBlobsBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, strings.Split(shas, ",")); err != nil { + ctx.Error(http.StatusBadRequest, "", err) + } else { + ctx.JSON(http.StatusOK, blobs) + } +} + // GetBlob get the blob of a repository file. func GetBlob(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index e69eeea639..bde0efea4e 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -238,6 +238,13 @@ type swaggerGitBlob struct { Body api.GitBlob `json:"body"` } +// GitBlobList +// swagger:response GitBlobList +type swaggerGitBlobList struct { + // in: body + Body []api.GitBlob `json:"body"` +} + // Commit // swagger:response Commit type swaggerCommit struct { diff --git a/services/repository/files/content.go b/services/repository/files/content.go index d51e5a4361..3d2217df18 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -250,6 +250,23 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref return contentsResponse, nil } +// GetBlobsBySHA gets multiple GitBlobs of a repository by sha hash. +func GetBlobsBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, shas []string) ([]*api.GitBlob, error) { + if len(shas) > setting.API.MaxResponseItems { + shas = shas[:setting.API.MaxResponseItems] + } + + blobs := make([]*api.GitBlob, 0, len(shas)) + for _, sha := range shas { + blob, err := GetBlobBySHA(ctx, repo, gitRepo, sha) + if err != nil { + return nil, err + } + blobs = append(blobs, blob) + } + return blobs, nil +} + // GetBlobBySHA get the GitBlob of a repository using a sha hash. func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlob, error) { gitBlob, err := gitRepo.GetBlob(sha) diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index 70b79bb58d..8fc8f56b4f 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -202,3 +202,43 @@ func TestGetBlobBySHA(t *testing.T) { require.NoError(t, err) assert.Equal(t, expectedGBR, gbr) } + +func TestGetBlobsBySHA(t *testing.T) { + unittest.PrepareTestEnv(t) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + + gbr, err := GetBlobsBySHA(db.DefaultContext, repo, gitRepo, []string{ + "ea82fc8777a24b07c26b3a4bf4e2742c03733eab", // Home.md + "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", // line.svg + "26f842bcad37fa40a1bb34cbb5ee219ee35d863d", // test.xml + }) + expectedGBR := []*api.GitBlob{ + { + Content: "IyBIb21lIHBhZ2UKClRoaXMgaXMgdGhlIGhvbWUgcGFnZSEK", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/ea82fc8777a24b07c26b3a4bf4e2742c03733eab", + SHA: "ea82fc8777a24b07c26b3a4bf4e2742c03733eab", + Size: 36, + }, + { + Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZwogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHdpZHRoPSIxMjgiCiAgIGhlaWdodD0iMTI4IgogICB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+CgogIDxsaW5lIHgxPSIwIiB5MT0iNyIgeDI9IjEwIiB5Mj0iNyIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", + SHA: "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", + Size: 246, + }, + { + Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHRlc3Q+VGhpcyBpcyBYTUw8L3Rlc3Q+Cg==", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/26f842bcad37fa40a1bb34cbb5ee219ee35d863d", + SHA: "26f842bcad37fa40a1bb34cbb5ee219ee35d863d", + Size: 64, + }, + } + require.NoError(t, err) + assert.Equal(t, expectedGBR, gbr) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1b5f55f97b..124d5bd1bd 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7616,6 +7616,49 @@ } } }, + "/repos/{owner}/{repo}/git/blobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets multiplbe blobs of a repository.", + "operationId": "GetBlobs", + "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 + }, + { + "type": "string", + "description": "a comma separated list of blob-sha (mind the overall URL-length limit of ~2,083 chars)", + "name": "shas", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/GitBlobList" + }, + "400": { + "$ref": "#/responses/error" + } + } + } + }, "/repos/{owner}/{repo}/git/blobs/{sha}": { "get": { "produces": [ @@ -7643,7 +7686,7 @@ }, { "type": "string", - "description": "sha of the commit", + "description": "sha of the blob to retrieve", "name": "sha", "in": "path", "required": true @@ -7651,7 +7694,7 @@ ], "responses": { "200": { - "$ref": "#/responses/GitBlobResponse" + "$ref": "#/responses/GitBlob" }, "400": { "$ref": "#/responses/error" @@ -24864,8 +24907,8 @@ }, "x-go-package": "forgejo.org/modules/structs" }, - "GitBlobResponse": { - "description": "GitBlobResponse represents a git blob", + "GitBlob": { + "description": "GitBlob represents a git blob", "type": "object", "properties": { "content": { @@ -29119,10 +29162,19 @@ "$ref": "#/definitions/GeneralUISettings" } }, - "GitBlobResponse": { - "description": "GitBlobResponse", + "GitBlob": { + "description": "GitBlob", "schema": { - "$ref": "#/definitions/GitBlobResponse" + "$ref": "#/definitions/GitBlob" + } + }, + "GitBlobList": { + "description": "GitBlobList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/GitBlob" + } } }, "GitHook": {