From a4ea74020f77aadc50f1763ec932f208753c493d Mon Sep 17 00:00:00 2001 From: oliverpool Date: Fri, 13 Jun 2025 11:36:57 +0200 Subject: [PATCH] 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": {