This commit is contained in:
Denys Konovalov 2025-04-12 16:17:12 -04:00 committed by GitHub
commit 3440181522
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 304 additions and 1 deletions

View File

@ -176,3 +176,8 @@ type FileDeleteResponse struct {
Commit *FileCommitResponse `json:"commit"`
Verification *PayloadCommitVerification `json:"verification"`
}
// GetFilesOptions options for retrieving metadate and content of multiple files
type GetFilesOptions struct {
Files []string `json:"files" binding:"Required"`
}

View File

@ -1389,6 +1389,7 @@ func Routes() *web.Router {
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
}, reqToken())
}, reqRepoReader(unit.TypeCode))
m.Post("/files", context.ReferencesGitRepo(), context.RepoRefForAPI, bind(api.GetFilesOptions{}), reqRepoReader(unit.TypeCode), repo.GetFiles)
m.Get("/signing-key.gpg", misc.SigningKey)
m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics).

View File

@ -25,7 +25,9 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
@ -982,3 +984,69 @@ func GetContentsList(ctx *context.APIContext) {
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
GetContents(ctx)
}
// GetFiles Get the metadata and contents of requested files
func GetFiles(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/files repository repoGetFiles
// ---
// summary: Get the metadata and contents of requested files
// 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: ref
// in: query
// description: "The name of the commit/branch/tag. Default the repositorys default branch (usually master)"
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/GetFilesOptions"
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
apiOpts := web.GetForm(ctx).(*api.GetFilesOptions)
ref := ctx.FormTrim("ref")
if ref == "" {
ref = ctx.Repo.Repository.DefaultBranch
}
if !ctx.Repo.GitRepo.IsReferenceExist(ref) {
ctx.APIErrorNotFound("GetFiles", "ref does not exist")
return
}
files := apiOpts.Files
filesResponse := files_service.GetContentsListFromTrees(ctx, ctx.Repo.Repository, ref, files)
count := len(filesResponse)
listOpts := utils.GetListOptions(ctx)
filesResponse = util.PaginateSlice(filesResponse, listOpts.Page, listOpts.PageSize).([]*api.ContentsResponse)
ctx.SetTotalCountHeader(int64(count))
ctx.JSON(http.StatusOK, filesResponse)
}

View File

@ -118,6 +118,9 @@ type swaggerParameterBodies struct {
// in:body
EditAttachmentOptions api.EditAttachmentOptions
// in:body
GetFilesOptions api.GetFilesOptions
// in:body
ChangeFilesOptions api.ChangeFilesOptions

View File

@ -17,12 +17,17 @@ import (
"code.gitea.io/gitea/modules/util"
)
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
func GetContentsListFromTrees(ctx context.Context, repo *repo_model.Repository, branch string, treeNames []string) []*api.ContentsResponse {
files := []*api.ContentsResponse{}
for _, file := range treeNames {
fileContents, _ := GetContents(ctx, repo, file, branch, false) // ok if fails, then will be nil
files = append(files, fileContents)
}
return files
}
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
files := GetContentsListFromTrees(ctx, repo, branch, treeNames)
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
verification := GetPayloadCommitVerification(ctx, commit)
filesResponse := &api.FilesResponse{

View File

@ -6771,6 +6771,68 @@
}
}
},
"/repos/{owner}/{repo}/files": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get the metadata and contents of requested files",
"operationId": "repoGetFiles",
"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": "The name of the commit/branch/tag. Default the repositorys default branch (usually master)",
"name": "ref",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/GetFilesOptions"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/ContentsListResponse"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/forks": {
"get": {
"produces": [
@ -23018,6 +23080,20 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"GetFilesOptions": {
"description": "GetFilesOptions options for retrieving metadate and content of multiple files",
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Files"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"GitBlobResponse": {
"description": "GitBlobResponse represents a git blob",
"type": "object",

View File

@ -0,0 +1,145 @@
// 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"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
)
func getExpectedcontentsListResponseForFiles(ref, refType, lastCommitSHA string) []*api.ContentsResponse {
return []*api.ContentsResponse{getExpectedContentsResponseForContents(ref, refType, lastCommitSHA)}
}
func TestAPIGetRequestedFiles(t *testing.T) {
onGiteaRun(t, testAPIGetRequestedFiles)
}
func testAPIGetRequestedFiles(t *testing.T, u *url.URL) {
/*** SETUP ***/
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
filesOptions := &api.GetFilesOptions{
Files: []string{
"README.md",
},
}
// Get user2's token req.Body =
session := loginUser(t, user2.Name)
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // TODO: allow for a POST-request to be scope read
// Get user4's token
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // TODO: allow for a POST-request to be scope read
// Get the commit ID of the default branch
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
assert.NoError(t, err)
defer gitRepo.Close()
// Make a new branch in repo1
newBranch := "test_branch"
err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
assert.NoError(t, err)
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
assert.NoError(t, err)
// Make a new tag in repo1
newTag := "test_tag"
err = gitRepo.CreateTag(newTag, commitID)
assert.NoError(t, err)
/*** END SETUP ***/
// ref is default ref
ref := repo1.DefaultBranch
refType := "branch"
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files?ref=%s", user2.Name, repo1.Name, ref), &filesOptions)
resp := MakeRequest(t, req, http.StatusOK)
var contentsListResponse []*api.ContentsResponse
DecodeJSON(t, resp, &contentsListResponse)
assert.NotNil(t, contentsListResponse)
lastCommit, _ := gitRepo.GetCommitByPath("README.md")
expectedcontentsListResponse := getExpectedcontentsListResponseForFiles(ref, refType, lastCommit.ID.String())
assert.Equal(t, expectedcontentsListResponse, contentsListResponse)
// No ref
refType = "branch"
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, repo1.Name), &filesOptions)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &contentsListResponse)
assert.NotNil(t, contentsListResponse)
expectedcontentsListResponse = getExpectedcontentsListResponseForFiles(repo1.DefaultBranch, refType, lastCommit.ID.String())
assert.Equal(t, expectedcontentsListResponse, contentsListResponse)
// ref is the branch we created above in setup
ref = newBranch
refType = "branch"
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files?ref=%s", user2.Name, repo1.Name, ref), &filesOptions)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &contentsListResponse)
assert.NotNil(t, contentsListResponse)
branchCommit, _ := gitRepo.GetBranchCommit(ref)
lastCommit, _ = branchCommit.GetCommitByPath("README.md")
expectedcontentsListResponse = getExpectedcontentsListResponseForFiles(ref, refType, lastCommit.ID.String())
assert.Equal(t, expectedcontentsListResponse, contentsListResponse)
// ref is the new tag we created above in setup
ref = newTag
refType = "tag"
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files?ref=%s", user2.Name, repo1.Name, ref), &filesOptions)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &contentsListResponse)
assert.NotNil(t, contentsListResponse)
tagCommit, _ := gitRepo.GetTagCommit(ref)
lastCommit, _ = tagCommit.GetCommitByPath("README.md")
expectedcontentsListResponse = getExpectedcontentsListResponseForFiles(ref, refType, lastCommit.ID.String())
assert.Equal(t, expectedcontentsListResponse, contentsListResponse)
// ref is a commit
ref = commitID
refType = "commit"
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files?ref=%s", user2.Name, repo1.Name, ref), &filesOptions)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &contentsListResponse)
assert.NotNil(t, contentsListResponse)
expectedcontentsListResponse = getExpectedcontentsListResponseForFiles(ref, refType, commitID)
assert.Equal(t, expectedcontentsListResponse, contentsListResponse)
// Test file contents a file with a bad ref
ref = "badref"
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files?ref=%s", user2.Name, repo1.Name, ref), &filesOptions)
MakeRequest(t, req, http.StatusNotFound)
// Test accessing private ref with user token that does not have access - should fail
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, repo16.Name), &filesOptions).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusNotFound)
// Test access private ref of owner of token
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", user2.Name, repo16.Name), &filesOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusOK)
// Test access of org org3 private repo file by owner user2
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/files", org3.Name, repo3.Name), &filesOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusOK)
}