mirror of
https://github.com/go-gitea/gitea
synced 2025-07-03 17:17:19 +00:00
Refactor repo contents API and add "contents-ext" API (#34822)
See the updated swagger document for details.
This commit is contained in:
@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@ -63,33 +64,37 @@ func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
|
// GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string
|
||||||
func (b *Blob) GetBlobContentBase64() (string, error) {
|
func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) {
|
||||||
dataRc, err := b.DataAsync()
|
dataRc, err := b.DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer dataRc.Close()
|
defer dataRc.Close()
|
||||||
|
|
||||||
pr, pw := io.Pipe()
|
base64buf := &strings.Builder{}
|
||||||
encoder := base64.NewEncoder(base64.StdEncoding, pw)
|
encoder := base64.NewEncoder(base64.StdEncoding, base64buf)
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
go func() {
|
loop:
|
||||||
_, err := io.Copy(encoder, dataRc)
|
for {
|
||||||
_ = encoder.Close()
|
n, err := dataRc.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
if err != nil {
|
if originContent != nil {
|
||||||
_ = pw.CloseWithError(err)
|
_, _ = originContent.Write(buf[:n])
|
||||||
} else {
|
|
||||||
_ = pw.Close()
|
|
||||||
}
|
}
|
||||||
}()
|
if _, err := encoder.Write(buf[:n]); err != nil {
|
||||||
|
|
||||||
out, err := io.ReadAll(pr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return string(out), nil
|
}
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, io.EOF):
|
||||||
|
break loop
|
||||||
|
case err != nil:
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = encoder.Close()
|
||||||
|
return base64buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GuessContentType guesses the content type of the blob.
|
// GuessContentType guesses the content type of the blob.
|
||||||
|
@ -18,7 +18,7 @@ type TreeEntry struct {
|
|||||||
sized bool
|
sized bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the entry
|
// Name returns the name of the entry (base name)
|
||||||
func (te *TreeEntry) Name() string {
|
func (te *TreeEntry) Name() string {
|
||||||
return te.name
|
return te.name
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
|
||||||
const (
|
const (
|
||||||
blobSizeCutoff = 1024
|
MetaFileMaxSize = 1024 // spec says the maximum size of a pointer file must be smaller than 1024
|
||||||
|
|
||||||
// MetaFileIdentifier is the string appearing at the first line of LFS pointer files.
|
MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" // the first line of a pointer file
|
||||||
// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
|
|
||||||
MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1"
|
|
||||||
|
|
||||||
// MetaFileOidPrefix appears in LFS pointer files on a line before the sha256 hash.
|
MetaFileOidPrefix = "oid sha256:" // spec says the only supported hash is sha256 at the moment
|
||||||
MetaFileOidPrefix = "oid sha256:"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -39,7 +37,7 @@ var (
|
|||||||
|
|
||||||
// ReadPointer tries to read LFS pointer data from the reader
|
// ReadPointer tries to read LFS pointer data from the reader
|
||||||
func ReadPointer(reader io.Reader) (Pointer, error) {
|
func ReadPointer(reader io.Reader) (Pointer, error) {
|
||||||
buf := make([]byte, blobSizeCutoff)
|
buf := make([]byte, MetaFileMaxSize)
|
||||||
n, err := io.ReadFull(reader, buf)
|
n, err := io.ReadFull(reader, buf)
|
||||||
if err != nil && err != io.ErrUnexpectedEOF {
|
if err != nil && err != io.ErrUnexpectedEOF {
|
||||||
return Pointer{}, err
|
return Pointer{}, err
|
||||||
@ -65,6 +63,7 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) {
|
|||||||
return p, ErrInvalidStructure
|
return p, ErrInvalidStructure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spec says "key/value pairs MUST be sorted alphabetically in ascending order (version is exception and must be the first)"
|
||||||
oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix)
|
oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix)
|
||||||
if len(oid) != 64 || !oidPattern.MatchString(oid) {
|
if len(oid) != 64 || !oidPattern.MatchString(oid) {
|
||||||
return p, ErrInvalidOIDFormat
|
return p, ErrInvalidOIDFormat
|
||||||
|
@ -31,7 +31,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
if blob.Size > blobSizeCutoff {
|
if blob.Size > MetaFileMaxSize {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,4 +10,7 @@ type GitBlobResponse struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
|
LfsOid *string `json:"lfs_oid,omitempty"`
|
||||||
|
LfsSize *int64 `json:"lfs_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,11 @@ type FileLinksResponse struct {
|
|||||||
HTMLURL *string `json:"html"`
|
HTMLURL *string `json:"html"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContentsExtResponse struct {
|
||||||
|
FileContents *ContentsResponse `json:"file_contents,omitempty"`
|
||||||
|
DirContents []*ContentsResponse `json:"dir_contents,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
|
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
|
||||||
type ContentsResponse struct {
|
type ContentsResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -145,6 +150,9 @@ type ContentsResponse struct {
|
|||||||
// `submodule_git_url` is populated when `type` is `submodule`, otherwise null
|
// `submodule_git_url` is populated when `type` is `submodule`, otherwise null
|
||||||
SubmoduleGitURL *string `json:"submodule_git_url"`
|
SubmoduleGitURL *string `json:"submodule_git_url"`
|
||||||
Links *FileLinksResponse `json:"_links"`
|
Links *FileLinksResponse `json:"_links"`
|
||||||
|
|
||||||
|
LfsOid *string `json:"lfs_oid"`
|
||||||
|
LfsSize *int64 `json:"lfs_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileCommitResponse contains information generated from a Git commit for a repo's file.
|
// FileCommitResponse contains information generated from a Git commit for a repo's file.
|
||||||
|
@ -1435,6 +1435,10 @@ func Routes() *web.Router {
|
|||||||
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
|
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||||
|
m.Group("/contents-ext", func() {
|
||||||
|
m.Get("", repo.GetContentsExt)
|
||||||
|
m.Get("/*", repo.GetContentsExt)
|
||||||
|
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||||
m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()).
|
m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()).
|
||||||
Get(repo.GetFileContentsGet).
|
Get(repo.GetFileContentsGet).
|
||||||
Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above
|
Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above
|
||||||
|
@ -47,7 +47,7 @@ func GetBlob(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
|
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
|
||||||
ctx.APIError(http.StatusBadRequest, err)
|
ctx.APIError(http.StatusBadRequest, err)
|
||||||
} else {
|
} else {
|
||||||
ctx.JSON(http.StatusOK, blob)
|
ctx.JSON(http.StatusOK, blob)
|
||||||
|
@ -905,11 +905,71 @@ func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int
|
|||||||
return refCommit
|
return refCommit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetContentsExt(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
|
||||||
|
// ---
|
||||||
|
// summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
|
||||||
|
// description: It guarantees that only one of the response fields is set if the request succeeds.
|
||||||
|
// Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
|
||||||
|
// 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: filepath
|
||||||
|
// in: path
|
||||||
|
// description: path of the dir, file, symlink or submodule in the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: ref
|
||||||
|
// in: query
|
||||||
|
// description: the name of the commit/branch/tag, default to the repository’s default branch.
|
||||||
|
// type: string
|
||||||
|
// required: false
|
||||||
|
// - name: includes
|
||||||
|
// in: query
|
||||||
|
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
|
||||||
|
// Option "file_content" will try to retrieve the file content, option "lfs_metadata" will try to retrieve LFS metadata.
|
||||||
|
// type: string
|
||||||
|
// required: false
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/ContentsExtResponse"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
|
||||||
|
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
|
||||||
|
if includeOpt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch includeOpt {
|
||||||
|
case "file_content":
|
||||||
|
opts.IncludeSingleFileContent = true
|
||||||
|
case "lfs_metadata":
|
||||||
|
opts.IncludeLfsMetadata = true
|
||||||
|
default:
|
||||||
|
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, getRepoContents(ctx, opts))
|
||||||
|
}
|
||||||
|
|
||||||
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
|
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
|
||||||
func GetContents(ctx *context.APIContext) {
|
func GetContents(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
|
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
|
||||||
// ---
|
// ---
|
||||||
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
|
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
|
||||||
|
// description: This API follows GitHub's design, and it is not easy to use. Recommend to use our "contents-ext" API instead.
|
||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
// parameters:
|
// parameters:
|
||||||
@ -938,29 +998,35 @@ func GetContents(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/ContentsResponse"
|
// "$ref": "#/responses/ContentsResponse"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true})
|
||||||
treePath := ctx.PathParam("*")
|
|
||||||
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
|
||||||
|
}
|
||||||
|
|
||||||
if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath); err != nil {
|
func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse {
|
||||||
|
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
|
||||||
|
if ctx.Written() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
|
||||||
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
ctx.APIErrorNotFound("GetContentsOrList", err)
|
ctx.APIErrorNotFound("GetContentsOrList", err)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
} else {
|
|
||||||
ctx.JSON(http.StatusOK, fileList)
|
|
||||||
}
|
}
|
||||||
|
return &ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContentsList Get the metadata of all the entries of the root dir
|
// GetContentsList Get the metadata of all the entries of the root dir
|
||||||
func GetContentsList(ctx *context.APIContext) {
|
func GetContentsList(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
|
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
|
||||||
// ---
|
// ---
|
||||||
// summary: Gets the metadata of all the entries of the root dir
|
// summary: Gets the metadata of all the entries of the root dir.
|
||||||
|
// description: This API follows GitHub's design, and it is not easy to use. Recommend to use our "contents-ext" API instead.
|
||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
// parameters:
|
// parameters:
|
||||||
@ -1084,6 +1150,6 @@ func handleGetFileContents(ctx *context.APIContext) {
|
|||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, refCommit, opts.Files)
|
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files)
|
||||||
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
|
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
|
||||||
}
|
}
|
||||||
|
@ -499,7 +499,7 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
|
|||||||
if blob.Size() > setting.API.DefaultMaxBlobSize {
|
if blob.Size() > setting.API.DefaultMaxBlobSize {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
content, err := blob.GetBlobContentBase64()
|
content, err := blob.GetBlobContentBase64(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return ""
|
return ""
|
||||||
|
@ -331,6 +331,12 @@ type swaggerContentsListResponse struct {
|
|||||||
Body []api.ContentsResponse `json:"body"`
|
Body []api.ContentsResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:response ContentsExtResponse
|
||||||
|
type swaggerContentsExtResponse struct {
|
||||||
|
// in:body
|
||||||
|
Body api.ContentsExtResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// FileDeleteResponse
|
// FileDeleteResponse
|
||||||
// swagger:response FileDeleteResponse
|
// swagger:response FileDeleteResponse
|
||||||
type swaggerFileDeleteResponse struct {
|
type swaggerFileDeleteResponse struct {
|
||||||
|
@ -5,13 +5,14 @@ package files
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@ -34,54 +35,50 @@ func (ct *ContentType) String() string {
|
|||||||
return string(*ct)
|
return string(*ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetContentsOrListOptions struct {
|
||||||
|
TreePath string
|
||||||
|
IncludeSingleFileContent bool // include the file's content when the tree path is a file
|
||||||
|
IncludeLfsMetadata bool
|
||||||
|
}
|
||||||
|
|
||||||
// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
|
// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
|
||||||
// directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
|
// directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
|
||||||
func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string) (any, error) {
|
func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) {
|
||||||
if repo.IsEmpty {
|
entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
|
||||||
return make([]any, 0), nil
|
if repo.IsEmpty && opts.TreePath == "" {
|
||||||
|
return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the path given in opts.treePath is valid (not a git path)
|
|
||||||
cleanTreePath := CleanGitTreePath(treePath)
|
|
||||||
if cleanTreePath == "" && treePath != "" {
|
|
||||||
return nil, ErrFilenameInvalid{
|
|
||||||
Path: treePath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
treePath = cleanTreePath
|
|
||||||
|
|
||||||
// Get the commit object for the ref
|
|
||||||
commit := refCommit.Commit
|
|
||||||
|
|
||||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get file contents
|
||||||
if entry.Type() != "tree" {
|
if entry.Type() != "tree" {
|
||||||
return GetContents(ctx, repo, refCommit, treePath, false)
|
ret.FileContents, err = getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
|
||||||
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are in a directory, so we return a list of FileContentResponse objects
|
// list directory contents
|
||||||
var fileList []*api.ContentsResponse
|
gitTree, err := refCommit.Commit.SubTree(opts.TreePath)
|
||||||
|
|
||||||
gitTree, err := commit.SubTree(treePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ret, err
|
||||||
}
|
}
|
||||||
entries, err := gitTree.ListEntries()
|
entries, err := gitTree.ListEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
ret.DirContents = make([]*api.ContentsResponse, 0, len(entries))
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
subTreePath := path.Join(treePath, e.Name())
|
subOpts := opts
|
||||||
fileContentResponse, err := GetContents(ctx, repo, refCommit, subTreePath, true)
|
subOpts.TreePath = path.Join(opts.TreePath, e.Name())
|
||||||
|
subOpts.IncludeSingleFileContent = false // never include file content when listing a directory
|
||||||
|
fileContentResponse, err := GetFileContents(ctx, repo, gitRepo, refCommit, subOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ret, err
|
||||||
}
|
}
|
||||||
fileList = append(fileList, fileContentResponse)
|
ret.DirContents = append(ret.DirContents, fileContentResponse)
|
||||||
}
|
}
|
||||||
return fileList, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectTypeFromTreeEntry check what content is behind it
|
// GetObjectTypeFromTreeEntry check what content is behind it
|
||||||
@ -100,35 +97,36 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
|
func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) {
|
||||||
func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) {
|
|
||||||
// Check that the path given in opts.treePath is valid (not a git path)
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
cleanTreePath := CleanGitTreePath(treePath)
|
cleanTreePath := CleanGitTreePath(*treePath)
|
||||||
if cleanTreePath == "" && treePath != "" {
|
if cleanTreePath == "" && *treePath != "" {
|
||||||
return nil, ErrFilenameInvalid{
|
return nil, ErrFilenameInvalid{Path: *treePath}
|
||||||
Path: treePath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
treePath = cleanTreePath
|
|
||||||
|
|
||||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer closer.Close()
|
|
||||||
|
|
||||||
commit := refCommit.Commit
|
|
||||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
*treePath = cleanTreePath
|
||||||
|
|
||||||
|
// Only allow safe ref types
|
||||||
refType := refCommit.RefName.RefType()
|
refType := refCommit.RefName.RefType()
|
||||||
if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit {
|
if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit {
|
||||||
return nil, fmt.Errorf("no commit found for the ref [ref: %s]", refCommit.RefName)
|
return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName)
|
||||||
}
|
}
|
||||||
|
|
||||||
selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(refCommit.InputRef))
|
return refCommit.Commit.GetTreeEntryByPath(*treePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
|
||||||
|
func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
|
||||||
|
entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
|
||||||
|
refType := refCommit.RefName.RefType()
|
||||||
|
commit := refCommit.Commit
|
||||||
|
selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(opts.TreePath) + "?ref=" + url.QueryEscape(refCommit.InputRef))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -139,7 +137,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCommit, err := commit.GetCommitByPath(treePath)
|
lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -147,7 +145,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
// All content types have these fields in populated
|
// All content types have these fields in populated
|
||||||
contentsResponse := &api.ContentsResponse{
|
contentsResponse := &api.ContentsResponse{
|
||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Path: treePath,
|
Path: opts.TreePath,
|
||||||
SHA: entry.ID.String(),
|
SHA: entry.ID.String(),
|
||||||
LastCommitSHA: lastCommit.ID.String(),
|
LastCommitSHA: lastCommit.ID.String(),
|
||||||
Size: entry.Size(),
|
Size: entry.Size(),
|
||||||
@ -170,13 +168,18 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
if entry.IsRegular() || entry.IsExecutable() {
|
if entry.IsRegular() || entry.IsExecutable() {
|
||||||
contentsResponse.Type = string(ContentTypeRegular)
|
contentsResponse.Type = string(ContentTypeRegular)
|
||||||
// if it is listing the repo root dir, don't waste system resources on reading content
|
// if it is listing the repo root dir, don't waste system resources on reading content
|
||||||
if !forList {
|
if opts.IncludeSingleFileContent {
|
||||||
blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String())
|
blobResponse, err := GetBlobBySHA(repo, gitRepo, entry.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contentsResponse.Encoding, contentsResponse.Content = blobResponse.Encoding, blobResponse.Content
|
||||||
|
contentsResponse.LfsOid, contentsResponse.LfsSize = blobResponse.LfsOid, blobResponse.LfsSize
|
||||||
|
} else if opts.IncludeLfsMetadata {
|
||||||
|
contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(gitRepo, entry.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
contentsResponse.Encoding = blobResponse.Encoding
|
|
||||||
contentsResponse.Content = blobResponse.Content
|
|
||||||
}
|
}
|
||||||
} else if entry.IsDir() {
|
} else if entry.IsDir() {
|
||||||
contentsResponse.Type = string(ContentTypeDir)
|
contentsResponse.Type = string(ContentTypeDir)
|
||||||
@ -190,7 +193,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
contentsResponse.Target = &targetFromContent
|
contentsResponse.Target = &targetFromContent
|
||||||
} else if entry.IsSubModule() {
|
} else if entry.IsSubModule() {
|
||||||
contentsResponse.Type = string(ContentTypeSubmodule)
|
contentsResponse.Type = string(ContentTypeSubmodule)
|
||||||
submodule, err := commit.GetSubModule(treePath)
|
submodule, err := commit.GetSubModule(opts.TreePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -200,7 +203,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
}
|
}
|
||||||
// Handle links
|
// Handle links
|
||||||
if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
|
if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
|
||||||
downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(treePath))
|
downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -208,7 +211,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
contentsResponse.DownloadURL = &downloadURLString
|
contentsResponse.DownloadURL = &downloadURLString
|
||||||
}
|
}
|
||||||
if !entry.IsSubModule() {
|
if !entry.IsSubModule() {
|
||||||
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(treePath))
|
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -228,8 +231,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut
|
|||||||
return contentsResponse, nil
|
return contentsResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
|
func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
|
||||||
func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
|
|
||||||
gitBlob, err := gitRepo.GetBlob(sha)
|
gitBlob, err := gitRepo.GetBlob(sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -239,12 +241,49 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
|
|||||||
URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
|
URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
|
||||||
Size: gitBlob.Size(),
|
Size: gitBlob.Size(),
|
||||||
}
|
}
|
||||||
if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
|
|
||||||
content, err := gitBlob.GetBlobContentBase64()
|
blobSize := gitBlob.Size()
|
||||||
|
if blobSize > setting.API.DefaultMaxBlobSize {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var originContent *strings.Builder
|
||||||
|
if 0 < blobSize && blobSize < lfs.MetaFileMaxSize {
|
||||||
|
originContent = &strings.Builder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := gitBlob.GetBlobContentBase64(originContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Encoding, ret.Content = util.ToPointer("base64"), &content
|
ret.Encoding, ret.Content = util.ToPointer("base64"), &content
|
||||||
|
if originContent != nil {
|
||||||
|
ret.LfsOid, ret.LfsSize = parsePossibleLfsPointerBuffer(strings.NewReader(originContent.String()))
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePossibleLfsPointerBuffer(r io.Reader) (*string, *int64) {
|
||||||
|
p, _ := lfs.ReadPointer(r)
|
||||||
|
if p.IsValid() {
|
||||||
|
return &p.Oid, &p.Size
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePossibleLfsPointerBlob(gitRepo *git.Repository, sha string) (*string, *int64, error) {
|
||||||
|
gitBlob, err := gitRepo.GetBlob(sha)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if gitBlob.Size() > lfs.MetaFileMaxSize {
|
||||||
|
return nil, nil, nil // not a LFS pointer
|
||||||
|
}
|
||||||
|
buf, err := gitBlob.GetBlobContent(lfs.MetaFileMaxSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
oid, size := parsePossibleLfsPointerBuffer(strings.NewReader(buf))
|
||||||
|
return oid, size, nil
|
||||||
|
}
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
@ -65,138 +64,50 @@ func TestGetContents(t *testing.T) {
|
|||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
contexttest.LoadGitRepo(t, ctx)
|
||||||
defer ctx.Repo.GitRepo.Close()
|
defer ctx.Repo.GitRepo.Close()
|
||||||
|
repo, gitRepo := ctx.Repo.Repository, ctx.Repo.GitRepo
|
||||||
treePath := "README.md"
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("GetContentsOrList(README.md)-MetaOnly", func(t *testing.T) {
|
||||||
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
||||||
|
expectedContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
|
||||||
t.Run("Get README.md contents with GetContents(ctx, )", func(t *testing.T) {
|
expectedContentsResponse.Content = nil
|
||||||
fileContentResponse, err := GetContents(ctx, ctx.Repo.Repository, refCommit, treePath, false)
|
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: false})
|
||||||
assert.Equal(t, expectedContentsResponse, fileContentResponse)
|
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetContentsOrListForDir(t *testing.T) {
|
t.Run("GetContentsOrList(README.md)", func(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: true})
|
||||||
ctx.SetPathParam("id", "1")
|
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
assert.NoError(t, err)
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
})
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
treePath := "" // root dir
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
|
t.Run("GetContentsOrList(RootDir)", func(t *testing.T) {
|
||||||
readmeContentsResponse := getExpectedReadmeContentsResponse()
|
readmeContentsResponse := getExpectedReadmeContentsResponse()
|
||||||
// because will be in a list, doesn't have encoding and content
|
readmeContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
|
||||||
readmeContentsResponse.Encoding = nil
|
|
||||||
readmeContentsResponse.Content = nil
|
readmeContentsResponse.Content = nil
|
||||||
|
expectedContentsListResponse := []*api.ContentsResponse{readmeContentsResponse}
|
||||||
expectedContentsListResponse := []*api.ContentsResponse{
|
// even if IncludeFileContent is true, it has no effect for directory listing
|
||||||
readmeContentsResponse,
|
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "", IncludeSingleFileContent: true})
|
||||||
}
|
assert.Equal(t, expectedContentsListResponse, extResp.DirContents)
|
||||||
|
|
||||||
t.Run("Get root dir contents with GetContentsOrList(ctx, )", func(t *testing.T) {
|
|
||||||
fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath)
|
|
||||||
assert.EqualValues(t, expectedContentsListResponse, fileContentResponse)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetContentsOrListForFile(t *testing.T) {
|
t.Run("GetContentsOrList(NoSuchTreePath)", func(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "no-such/file.md"})
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetPathParam("id", "1")
|
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
treePath := "README.md"
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
expectedContentsResponse := getExpectedReadmeContentsResponse()
|
|
||||||
|
|
||||||
t.Run("Get README.md contents with GetContentsOrList(ctx, )", func(t *testing.T) {
|
|
||||||
fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath)
|
|
||||||
assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetContentsErrors(t *testing.T) {
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetPathParam("id", "1")
|
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("bad treePath", func(t *testing.T) {
|
|
||||||
badTreePath := "bad/tree.md"
|
|
||||||
fileContentResponse, err := GetContents(ctx, repo, refCommit, badTreePath, false)
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
|
assert.EqualError(t, err, "object does not exist [id: , rel_path: no-such]")
|
||||||
assert.Nil(t, fileContentResponse)
|
assert.Nil(t, extResp.DirContents)
|
||||||
|
assert.Nil(t, extResp.FileContents)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetContentsOrListErrors(t *testing.T) {
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
|
||||||
ctx.SetPathParam("id", "1")
|
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
|
||||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("bad treePath", func(t *testing.T) {
|
|
||||||
badTreePath := "bad/tree.md"
|
|
||||||
fileContentResponse, err := GetContentsOrList(ctx, repo, refCommit, badTreePath)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
|
|
||||||
assert.Nil(t, fileContentResponse)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBlobBySHA(t *testing.T) {
|
|
||||||
unittest.PrepareTestEnv(t)
|
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
|
||||||
contexttest.LoadRepoCommit(t, ctx)
|
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
|
||||||
contexttest.LoadGitRepo(t, ctx)
|
|
||||||
defer ctx.Repo.GitRepo.Close()
|
|
||||||
|
|
||||||
|
t.Run("GetBlobBySHA", func(t *testing.T) {
|
||||||
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||||
ctx.SetPathParam("id", "1")
|
ctx.SetPathParam("id", "1")
|
||||||
ctx.SetPathParam("sha", sha)
|
ctx.SetPathParam("sha", sha)
|
||||||
|
gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha"))
|
||||||
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
|
||||||
if err != nil {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam("sha"))
|
|
||||||
expectedGBR := &api.GitBlobResponse{
|
expectedGBR := &api.GitBlobResponse{
|
||||||
Content: util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"),
|
Content: util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"),
|
||||||
Encoding: util.ToPointer("base64"),
|
Encoding: util.ToPointer("base64"),
|
||||||
@ -206,4 +117,5 @@ func TestGetBlobBySHA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedGBR, gbr)
|
assert.Equal(t, expectedGBR, gbr)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ import (
|
|||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
|
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
|
||||||
var size int64
|
var size int64
|
||||||
for _, treePath := range treePaths {
|
for _, treePath := range treePaths {
|
||||||
fileContents, _ := GetContents(ctx, repo, refCommit, treePath, false) // ok if fails, then will be nil
|
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: treePath, IncludeSingleFileContent: true}) // ok if fails, then will be nil
|
||||||
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
|
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
|
||||||
// if content isn't empty (e.g. due to the single blob being too large), add file size to response size
|
// if content isn't empty (e.g., due to the single blob being too large), add file size to response size
|
||||||
size += int64(len(*fileContents.Content))
|
size += int64(len(*fileContents.Content))
|
||||||
}
|
}
|
||||||
if size > setting.API.DefaultMaxResponseSize {
|
if size > setting.API.DefaultMaxResponseSize {
|
||||||
@ -38,8 +38,8 @@ func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Reposito
|
|||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) {
|
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) {
|
||||||
files := GetContentsListFromTreePaths(ctx, repo, refCommit, treeNames)
|
files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames)
|
||||||
fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil
|
fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil
|
||||||
verification := GetPayloadCommitVerification(ctx, refCommit.Commit)
|
verification := GetPayloadCommitVerification(ctx, refCommit.Commit)
|
||||||
filesResponse := &api.FilesResponse{
|
filesResponse := &api.FilesResponse{
|
||||||
|
@ -315,7 +315,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||||||
|
|
||||||
// FIXME: this call seems not right, why it needs to read the file content again
|
// FIXME: this call seems not right, why it needs to read the file content again
|
||||||
// FIXME: why it uses the NewBranch as "ref", it should use the commit ID because the response is only for this commit
|
// FIXME: why it uses the NewBranch as "ref", it should use the commit ID because the response is only for this commit
|
||||||
filesResponse, err := GetFilesResponseFromCommit(ctx, repo, utils.NewRefCommit(git.RefNameFromBranch(opts.NewBranch), commit), treePaths)
|
filesResponse, err := GetFilesResponseFromCommit(ctx, repo, gitRepo, utils.NewRefCommit(git.RefNameFromBranch(opts.NewBranch), commit), treePaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
104
templates/swagger/v1_json.tmpl
generated
104
templates/swagger/v1_json.tmpl
generated
@ -7424,13 +7424,14 @@
|
|||||||
},
|
},
|
||||||
"/repos/{owner}/{repo}/contents": {
|
"/repos/{owner}/{repo}/contents": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"description": "This API follows GitHub's design, and it is not easy to use. Recommend to use our \"contents-ext\" API instead.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"repository"
|
"repository"
|
||||||
],
|
],
|
||||||
"summary": "Gets the metadata of all the entries of the root dir",
|
"summary": "Gets the metadata of all the entries of the root dir.",
|
||||||
"operationId": "repoGetContentsList",
|
"operationId": "repoGetContentsList",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -7518,15 +7519,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/repos/{owner}/{repo}/contents/{filepath}": {
|
"/repos/{owner}/{repo}/contents-ext/{filepath}": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"description": "It guarantees that only one of the response fields is set if the request succeeds. Users can pass \"includes=file_content\" or \"includes=lfs_metadata\" to retrieve more fields.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"repository"
|
"repository"
|
||||||
],
|
],
|
||||||
"summary": "Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir",
|
"summary": "The extended \"contents\" API, to get file metadata and/or content, or list a directory.",
|
||||||
|
"operationId": "repoGetContentsExt",
|
||||||
|
"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": "path of the dir, file, symlink or submodule in the repo",
|
||||||
|
"name": "filepath",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "the name of the commit/branch/tag, default to the repository’s default branch.",
|
||||||
|
"name": "ref",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "By default this API's response only contains file's metadata. Use comma-separated \"includes\" options to retrieve more fields. Option \"file_content\" will try to retrieve the file content, option \"lfs_metadata\" will try to retrieve LFS metadata.",
|
||||||
|
"name": "includes",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ContentsExtResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/contents/{filepath}": {
|
||||||
|
"get": {
|
||||||
|
"description": "This API follows GitHub's design, and it is not easy to use. Recommend to use our \"contents-ext\" API instead.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.",
|
||||||
"operationId": "repoGetContents",
|
"operationId": "repoGetContents",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -22255,6 +22313,22 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"ContentsExtResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dir_contents": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ContentsResponse"
|
||||||
|
},
|
||||||
|
"x-go-name": "DirContents"
|
||||||
|
},
|
||||||
|
"file_contents": {
|
||||||
|
"$ref": "#/definitions/ContentsResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"ContentsResponse": {
|
"ContentsResponse": {
|
||||||
"description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
|
"description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -22298,6 +22372,15 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"x-go-name": "LastCommitterDate"
|
"x-go-name": "LastCommitterDate"
|
||||||
},
|
},
|
||||||
|
"lfs_oid": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "LfsOid"
|
||||||
|
},
|
||||||
|
"lfs_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "LfsSize"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Name"
|
"x-go-name": "Name"
|
||||||
@ -24947,6 +25030,15 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Encoding"
|
"x-go-name": "Encoding"
|
||||||
},
|
},
|
||||||
|
"lfs_oid": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "LfsOid"
|
||||||
|
},
|
||||||
|
"lfs_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "LfsSize"
|
||||||
|
},
|
||||||
"sha": {
|
"sha": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "SHA"
|
"x-go-name": "SHA"
|
||||||
@ -28693,6 +28785,12 @@
|
|||||||
"$ref": "#/definitions/Compare"
|
"$ref": "#/definitions/Compare"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ContentsExtResponse": {
|
||||||
|
"description": "",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ContentsExtResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ContentsListResponse": {
|
"ContentsListResponse": {
|
||||||
"description": "ContentsListResponse",
|
"description": "ContentsListResponse",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -20,9 +21,9 @@ import (
|
|||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
"code.gitea.io/gitea/tests"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) *api.ContentsResponse {
|
func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) *api.ContentsResponse {
|
||||||
@ -54,7 +55,11 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIGetContents(t *testing.T) {
|
func TestAPIGetContents(t *testing.T) {
|
||||||
onGiteaRun(t, testAPIGetContents)
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
testAPIGetContentsRefFormats(t)
|
||||||
|
testAPIGetContents(t, u)
|
||||||
|
testAPIGetContentsExt(t)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIGetContents(t *testing.T, u *url.URL) {
|
func testAPIGetContents(t *testing.T, u *url.URL) {
|
||||||
@ -76,20 +81,20 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
|
|
||||||
// Get the commit ID of the default branch
|
// Get the commit ID of the default branch
|
||||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
|
|
||||||
// Make a new branch in repo1
|
// Make a new branch in repo1
|
||||||
newBranch := "test_branch"
|
newBranch := "test_branch"
|
||||||
err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// Make a new tag in repo1
|
// Make a new tag in repo1
|
||||||
newTag := "test_tag"
|
newTag := "test_tag"
|
||||||
err = gitRepo.CreateTag(newTag, commitID)
|
err = gitRepo.CreateTag(newTag, commitID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
/*** END SETUP ***/
|
/*** END SETUP ***/
|
||||||
|
|
||||||
// ref is default ref
|
// ref is default ref
|
||||||
@ -99,7 +104,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
var contentsResponse api.ContentsResponse
|
var contentsResponse api.ContentsResponse
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.NotNil(t, contentsResponse)
|
|
||||||
lastCommit, _ := gitRepo.GetCommitByPath("README.md")
|
lastCommit, _ := gitRepo.GetCommitByPath("README.md")
|
||||||
expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
||||||
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
||||||
@ -109,7 +113,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.NotNil(t, contentsResponse)
|
|
||||||
expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String())
|
expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String())
|
||||||
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
||||||
|
|
||||||
@ -119,7 +122,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.NotNil(t, contentsResponse)
|
|
||||||
branchCommit, _ := gitRepo.GetBranchCommit(ref)
|
branchCommit, _ := gitRepo.GetBranchCommit(ref)
|
||||||
lastCommit, _ = branchCommit.GetCommitByPath("README.md")
|
lastCommit, _ = branchCommit.GetCommitByPath("README.md")
|
||||||
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
||||||
@ -131,7 +133,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.NotNil(t, contentsResponse)
|
|
||||||
tagCommit, _ := gitRepo.GetTagCommit(ref)
|
tagCommit, _ := gitRepo.GetTagCommit(ref)
|
||||||
lastCommit, _ = tagCommit.GetCommitByPath("README.md")
|
lastCommit, _ = tagCommit.GetCommitByPath("README.md")
|
||||||
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
|
||||||
@ -143,7 +144,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &contentsResponse)
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
assert.NotNil(t, contentsResponse)
|
|
||||||
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID)
|
expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID)
|
||||||
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
assert.Equal(t, *expectedContentsResponse, contentsResponse)
|
||||||
|
|
||||||
@ -168,9 +168,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
|||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIGetContentsRefFormats(t *testing.T) {
|
func testAPIGetContentsRefFormats(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
file := "README.md"
|
file := "README.md"
|
||||||
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||||
content := "# repo1\n\nDescription for repo1"
|
content := "# repo1\n\nDescription for repo1"
|
||||||
@ -203,3 +201,76 @@ func TestAPIGetContentsRefFormats(t *testing.T) {
|
|||||||
// FIXME: this is an incorrect behavior, non-existing branch falls back to default branch
|
// FIXME: this is an incorrect behavior, non-existing branch falls back to default branch
|
||||||
_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=no-such"), http.StatusOK)
|
_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=no-such"), http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAPIGetContentsExt(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
t.Run("DirContents", func(t *testing.T) {
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var contentsResponse api.ContentsExtResponse
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
|
assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].Encoding)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].Content)
|
||||||
|
|
||||||
|
// "includes=file_content" shouldn't affect directory listing
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check&includes=file_content")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
|
assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].Encoding)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents[0].Content)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext?includes=file_content,lfs_metadata").AddTokenAuth(token2)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents)
|
||||||
|
respFileIdx := slices.IndexFunc(contentsResponse.DirContents, func(response *api.ContentsResponse) bool { return response.Name == "jpeg.jpg" })
|
||||||
|
require.NotEqual(t, -1, respFileIdx)
|
||||||
|
respFile := contentsResponse.DirContents[respFileIdx]
|
||||||
|
assert.Equal(t, "jpeg.jpg", respFile.Name)
|
||||||
|
assert.Nil(t, respFile.Encoding)
|
||||||
|
assert.Nil(t, respFile.Content)
|
||||||
|
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
||||||
|
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||||
|
})
|
||||||
|
t.Run("FileContents", func(t *testing.T) {
|
||||||
|
// by default, no file content is returned
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var contentsResponse api.ContentsExtResponse
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents)
|
||||||
|
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.Encoding)
|
||||||
|
assert.Nil(t, contentsResponse.FileContents.Content)
|
||||||
|
|
||||||
|
// file content is only returned when `includes=file_content`
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check&includes=file_content")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents)
|
||||||
|
assert.Equal(t, "README.md", contentsResponse.FileContents.Name)
|
||||||
|
assert.NotNil(t, contentsResponse.FileContents.Encoding)
|
||||||
|
assert.NotNil(t, contentsResponse.FileContents.Content)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext/jpeg.jpg?includes=file_content").AddTokenAuth(token2)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
contentsResponse = api.ContentsExtResponse{}
|
||||||
|
DecodeJSON(t, resp, &contentsResponse)
|
||||||
|
assert.Nil(t, contentsResponse.DirContents)
|
||||||
|
assert.NotNil(t, contentsResponse.FileContents)
|
||||||
|
respFile := contentsResponse.FileContents
|
||||||
|
assert.Equal(t, "jpeg.jpg", respFile.Name)
|
||||||
|
assert.NotNil(t, respFile.Encoding)
|
||||||
|
assert.NotNil(t, respFile.Content)
|
||||||
|
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
||||||
|
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -287,6 +287,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
|
|||||||
details := []struct {
|
details := []struct {
|
||||||
filename, sha, content string
|
filename, sha, content string
|
||||||
size int64
|
size int64
|
||||||
|
lfsOid *string
|
||||||
|
lfsSize *int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
filename: "README.txt",
|
filename: "README.txt",
|
||||||
@ -299,6 +301,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
|
|||||||
sha: "d4a41a0d4db4949e129bd22f871171ea988103ef",
|
sha: "d4a41a0d4db4949e129bd22f871171ea988103ef",
|
||||||
size: 129,
|
size: 129,
|
||||||
content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6MmVjY2RiNDM4MjVkMmE0OWQ5OWQ1NDJkYWEyMDA3NWNmZjFkOTdkOWQyMzQ5YTg5NzdlZmU5YzAzNjYxNzM3YwpzaXplIDIwNDgK",
|
content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6MmVjY2RiNDM4MjVkMmE0OWQ5OWQ1NDJkYWEyMDA3NWNmZjFkOTdkOWQyMzQ5YTg5NzdlZmU5YzAzNjYxNzM3YwpzaXplIDIwNDgK",
|
||||||
|
lfsOid: util.ToPointer("2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c"),
|
||||||
|
lfsSize: util.ToPointer(int64(2048)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
filename: "jpeg.jpeg",
|
filename: "jpeg.jpeg",
|
||||||
@ -311,6 +315,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
|
|||||||
sha: "2b6c6c4eaefa24b22f2092c3d54b263ff26feb58",
|
sha: "2b6c6c4eaefa24b22f2092c3d54b263ff26feb58",
|
||||||
size: 127,
|
size: 127,
|
||||||
content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6N2I2YjJjODhkYmE5Zjc2MGExYTU4NDY5YjY3ZmVlMmI2OThlZjdlOTM5OWM0Y2E0ZjM0YTE0Y2NiZTM5ZjYyMwpzaXplIDI3Cg==",
|
content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6N2I2YjJjODhkYmE5Zjc2MGExYTU4NDY5YjY3ZmVlMmI2OThlZjdlOTM5OWM0Y2E0ZjM0YTE0Y2NiZTM5ZjYyMwpzaXplIDI3Cg==",
|
||||||
|
lfsOid: util.ToPointer("7b6b2c88dba9f760a1a58469b67fee2b698ef7e9399c4ca4f34a14ccbe39f623"),
|
||||||
|
lfsSize: util.ToPointer(int64(27)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,6 +345,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str
|
|||||||
GitURL: &gitURL,
|
GitURL: &gitURL,
|
||||||
HTMLURL: &htmlURL,
|
HTMLURL: &htmlURL,
|
||||||
},
|
},
|
||||||
|
LfsOid: detail.lfsOid,
|
||||||
|
LfsSize: detail.lfsSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user