mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:45:25 +01:00 
			
		
		
		
	Refactor repo contents API and add "contents-ext" API (#34822)
See the updated swagger document for details.
This commit is contained in:
		
							parent
							
								
									7be1a5e585
								
							
						
					
					
						commit
						dbd9c69909
					
				| @ -9,6 +9,7 @@ import ( | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/typesniffer" | ||||
| 	"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 | ||||
| func (b *Blob) GetBlobContentBase64() (string, error) { | ||||
| // GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string | ||||
| func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) { | ||||
| 	dataRc, err := b.DataAsync() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer dataRc.Close() | ||||
| 
 | ||||
| 	pr, pw := io.Pipe() | ||||
| 	encoder := base64.NewEncoder(base64.StdEncoding, pw) | ||||
| 
 | ||||
| 	go func() { | ||||
| 		_, err := io.Copy(encoder, dataRc) | ||||
| 		_ = encoder.Close() | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			_ = pw.CloseWithError(err) | ||||
| 		} else { | ||||
| 			_ = pw.Close() | ||||
| 	base64buf := &strings.Builder{} | ||||
| 	encoder := base64.NewEncoder(base64.StdEncoding, base64buf) | ||||
| 	buf := make([]byte, 32*1024) | ||||
| loop: | ||||
| 	for { | ||||
| 		n, err := dataRc.Read(buf) | ||||
| 		if n > 0 { | ||||
| 			if originContent != nil { | ||||
| 				_, _ = originContent.Write(buf[:n]) | ||||
| 			} | ||||
| 			if _, err := encoder.Write(buf[:n]); err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
| 		} | ||||
| 		switch { | ||||
| 		case errors.Is(err, io.EOF): | ||||
| 			break loop | ||||
| 		case err != nil: | ||||
| 			return "", err | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	out, err := io.ReadAll(pr) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return string(out), nil | ||||
| 	_ = encoder.Close() | ||||
| 	return base64buf.String(), nil | ||||
| } | ||||
| 
 | ||||
| // GuessContentType guesses the content type of the blob. | ||||
|  | ||||
| @ -18,7 +18,7 @@ type TreeEntry struct { | ||||
| 	sized     bool | ||||
| } | ||||
| 
 | ||||
| // Name returns the name of the entry | ||||
| // Name returns the name of the entry (base name) | ||||
| func (te *TreeEntry) Name() string { | ||||
| 	return te.name | ||||
| } | ||||
|  | ||||
| @ -15,15 +15,13 @@ import ( | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md | ||||
| 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. | ||||
| 	// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md | ||||
| 	MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" | ||||
| 	MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" // the first line of a pointer file | ||||
| 
 | ||||
| 	// MetaFileOidPrefix appears in LFS pointer files on a line before the sha256 hash. | ||||
| 	MetaFileOidPrefix = "oid sha256:" | ||||
| 	MetaFileOidPrefix = "oid sha256:" // spec says the only supported hash is sha256 at the moment | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -39,7 +37,7 @@ var ( | ||||
| 
 | ||||
| // ReadPointer tries to read LFS pointer data from the reader | ||||
| func ReadPointer(reader io.Reader) (Pointer, error) { | ||||
| 	buf := make([]byte, blobSizeCutoff) | ||||
| 	buf := make([]byte, MetaFileMaxSize) | ||||
| 	n, err := io.ReadFull(reader, buf) | ||||
| 	if err != nil && err != io.ErrUnexpectedEOF { | ||||
| 		return Pointer{}, err | ||||
| @ -65,6 +63,7 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) { | ||||
| 		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) | ||||
| 	if len(oid) != 64 || !oidPattern.MatchString(oid) { | ||||
| 		return p, ErrInvalidOIDFormat | ||||
|  | ||||
| @ -31,7 +31,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c | ||||
| 			default: | ||||
| 			} | ||||
| 
 | ||||
| 			if blob.Size > blobSizeCutoff { | ||||
| 			if blob.Size > MetaFileMaxSize { | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
|  | ||||
| @ -10,4 +10,7 @@ type GitBlobResponse struct { | ||||
| 	URL      string  `json:"url"` | ||||
| 	SHA      string  `json:"sha"` | ||||
| 	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"` | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| type ContentsResponse struct { | ||||
| 	Name          string `json:"name"` | ||||
| @ -145,6 +150,9 @@ type ContentsResponse struct { | ||||
| 	// `submodule_git_url` is populated when `type` is `submodule`, otherwise null | ||||
| 	SubmoduleGitURL *string            `json:"submodule_git_url"` | ||||
| 	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. | ||||
|  | ||||
| @ -1435,6 +1435,10 @@ func Routes() *web.Router { | ||||
| 						m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) | ||||
| 					}, reqToken()) | ||||
| 				}, 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()). | ||||
| 					Get(repo.GetFileContentsGet). | ||||
| 					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 | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 	} else { | ||||
| 		ctx.JSON(http.StatusOK, blob) | ||||
|  | ||||
| @ -905,11 +905,71 @@ func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int | ||||
| 	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 | ||||
| func GetContents(ctx *context.APIContext) { | ||||
| 	// 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: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| @ -938,29 +998,35 @@ func GetContents(ctx *context.APIContext) { | ||||
| 	//     "$ref": "#/responses/ContentsResponse" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 
 | ||||
| 	treePath := ctx.PathParam("*") | ||||
| 	refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref")) | ||||
| 	ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true}) | ||||
| 	if ctx.Written() { | ||||
| 		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) { | ||||
| 			ctx.APIErrorNotFound("GetContentsOrList", err) | ||||
| 			return | ||||
| 			return nil | ||||
| 		} | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 	} else { | ||||
| 		ctx.JSON(http.StatusOK, fileList) | ||||
| 	} | ||||
| 	return &ret | ||||
| } | ||||
| 
 | ||||
| // GetContentsList Get the metadata of all the entries of the root dir | ||||
| func GetContentsList(ctx *context.APIContext) { | ||||
| 	// 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: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| @ -1084,6 +1150,6 @@ func handleGetFileContents(ctx *context.APIContext) { | ||||
| 	if ctx.Written() { | ||||
| 		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)) | ||||
| } | ||||
|  | ||||
| @ -499,7 +499,7 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { | ||||
| 	if blob.Size() > setting.API.DefaultMaxBlobSize { | ||||
| 		return "" | ||||
| 	} | ||||
| 	content, err := blob.GetBlobContentBase64() | ||||
| 	content, err := blob.GetBlobContentBase64(nil) | ||||
| 	if err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return "" | ||||
|  | ||||
| @ -331,6 +331,12 @@ type swaggerContentsListResponse struct { | ||||
| 	Body []api.ContentsResponse `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // swagger:response ContentsExtResponse | ||||
| type swaggerContentsExtResponse struct { | ||||
| 	// in:body | ||||
| 	Body api.ContentsExtResponse `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // FileDeleteResponse | ||||
| // swagger:response FileDeleteResponse | ||||
| type swaggerFileDeleteResponse struct { | ||||
|  | ||||
| @ -5,13 +5,14 @@ package files | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	"code.gitea.io/gitea/modules/lfs" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @ -34,54 +35,50 @@ func (ct *ContentType) String() string { | ||||
| 	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 | ||||
| // 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) { | ||||
| 	if repo.IsEmpty { | ||||
| 		return make([]any, 0), nil | ||||
| func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) { | ||||
| 	entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath) | ||||
| 	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 { | ||||
| 		return nil, err | ||||
| 		return ret, err | ||||
| 	} | ||||
| 
 | ||||
| 	// get file contents | ||||
| 	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 | ||||
| 	var fileList []*api.ContentsResponse | ||||
| 
 | ||||
| 	gitTree, err := commit.SubTree(treePath) | ||||
| 	// list directory contents | ||||
| 	gitTree, err := refCommit.Commit.SubTree(opts.TreePath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return ret, err | ||||
| 	} | ||||
| 	entries, err := gitTree.ListEntries() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return ret, err | ||||
| 	} | ||||
| 	ret.DirContents = make([]*api.ContentsResponse, 0, len(entries)) | ||||
| 	for _, e := range entries { | ||||
| 		subTreePath := path.Join(treePath, e.Name()) | ||||
| 		fileContentResponse, err := GetContents(ctx, repo, refCommit, subTreePath, true) | ||||
| 		subOpts := opts | ||||
| 		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 { | ||||
| 			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 | ||||
| @ -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 GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) { | ||||
| func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) { | ||||
| 	// 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 | ||||
| 
 | ||||
| 	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 | ||||
| 	cleanTreePath := CleanGitTreePath(*treePath) | ||||
| 	if cleanTreePath == "" && *treePath != "" { | ||||
| 		return nil, ErrFilenameInvalid{Path: *treePath} | ||||
| 	} | ||||
| 	*treePath = cleanTreePath | ||||
| 
 | ||||
| 	// Only allow safe ref types | ||||
| 	refType := refCommit.RefName.RefType() | ||||
| 	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 { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -139,7 +137,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	lastCommit, err := commit.GetCommitByPath(treePath) | ||||
| 	lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath) | ||||
| 	if err != nil { | ||||
| 		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 | ||||
| 	contentsResponse := &api.ContentsResponse{ | ||||
| 		Name:          entry.Name(), | ||||
| 		Path:          treePath, | ||||
| 		Path:          opts.TreePath, | ||||
| 		SHA:           entry.ID.String(), | ||||
| 		LastCommitSHA: lastCommit.ID.String(), | ||||
| 		Size:          entry.Size(), | ||||
| @ -170,13 +168,18 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 	if entry.IsRegular() || entry.IsExecutable() { | ||||
| 		contentsResponse.Type = string(ContentTypeRegular) | ||||
| 		// if it is listing the repo root dir, don't waste system resources on reading content | ||||
| 		if !forList { | ||||
| 			blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()) | ||||
| 		if opts.IncludeSingleFileContent { | ||||
| 			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 { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			contentsResponse.Encoding = blobResponse.Encoding | ||||
| 			contentsResponse.Content = blobResponse.Content | ||||
| 		} | ||||
| 	} else if entry.IsDir() { | ||||
| 		contentsResponse.Type = string(ContentTypeDir) | ||||
| @ -190,7 +193,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 		contentsResponse.Target = &targetFromContent | ||||
| 	} else if entry.IsSubModule() { | ||||
| 		contentsResponse.Type = string(ContentTypeSubmodule) | ||||
| 		submodule, err := commit.GetSubModule(treePath) | ||||
| 		submodule, err := commit.GetSubModule(opts.TreePath) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @ -200,7 +203,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 	} | ||||
| 	// Handle links | ||||
| 	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 { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @ -208,7 +211,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 		contentsResponse.DownloadURL = &downloadURLString | ||||
| 	} | ||||
| 	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 { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @ -228,8 +231,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut | ||||
| 	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) { | ||||
| func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { | ||||
| 	gitBlob, err := gitRepo.GetBlob(sha) | ||||
| 	if err != nil { | ||||
| 		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()), | ||||
| 		Size: gitBlob.Size(), | ||||
| 	} | ||||
| 	if gitBlob.Size() <= setting.API.DefaultMaxBlobSize { | ||||
| 		content, err := gitBlob.GetBlobContentBase64() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		ret.Encoding, ret.Content = util.ToPointer("base64"), &content | ||||
| 
 | ||||
| 	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 { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	ret.Encoding, ret.Content = util.ToPointer("base64"), &content | ||||
| 	if originContent != nil { | ||||
| 		ret.LfsOid, ret.LfsSize = parsePossibleLfsPointerBuffer(strings.NewReader(originContent.String())) | ||||
| 	} | ||||
| 	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" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| @ -65,145 +64,58 @@ func TestGetContents(t *testing.T) { | ||||
| 	contexttest.LoadUser(t, ctx, 2) | ||||
| 	contexttest.LoadGitRepo(t, ctx) | ||||
| 	defer ctx.Repo.GitRepo.Close() | ||||
| 
 | ||||
| 	treePath := "README.md" | ||||
| 	repo, gitRepo := ctx.Repo.Repository, ctx.Repo.GitRepo | ||||
| 	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 GetContents(ctx, )", func(t *testing.T) { | ||||
| 		fileContentResponse, err := GetContents(ctx, ctx.Repo.Repository, refCommit, treePath, false) | ||||
| 		assert.Equal(t, expectedContentsResponse, fileContentResponse) | ||||
| 	t.Run("GetContentsOrList(README.md)-MetaOnly", func(t *testing.T) { | ||||
| 		expectedContentsResponse := getExpectedReadmeContentsResponse() | ||||
| 		expectedContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content | ||||
| 		expectedContentsResponse.Content = nil | ||||
| 		extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: false}) | ||||
| 		assert.Equal(t, expectedContentsResponse, extResp.FileContents) | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestGetContentsOrListForDir(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() | ||||
| 
 | ||||
| 	treePath := "" // root dir | ||||
| 	refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	readmeContentsResponse := getExpectedReadmeContentsResponse() | ||||
| 	// because will be in a list, doesn't have encoding and content | ||||
| 	readmeContentsResponse.Encoding = nil | ||||
| 	readmeContentsResponse.Content = nil | ||||
| 
 | ||||
| 	expectedContentsListResponse := []*api.ContentsResponse{ | ||||
| 		readmeContentsResponse, | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 	t.Run("GetContentsOrList(README.md)", func(t *testing.T) { | ||||
| 		expectedContentsResponse := getExpectedReadmeContentsResponse() | ||||
| 		extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: true}) | ||||
| 		assert.Equal(t, expectedContentsResponse, extResp.FileContents) | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestGetContentsOrListForFile(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() | ||||
| 
 | ||||
| 	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) | ||||
| 	t.Run("GetContentsOrList(RootDir)", func(t *testing.T) { | ||||
| 		readmeContentsResponse := getExpectedReadmeContentsResponse() | ||||
| 		readmeContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content | ||||
| 		readmeContentsResponse.Content = nil | ||||
| 		expectedContentsListResponse := []*api.ContentsResponse{readmeContentsResponse} | ||||
| 		// even if IncludeFileContent is true, it has no effect for directory listing | ||||
| 		extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "", IncludeSingleFileContent: true}) | ||||
| 		assert.Equal(t, expectedContentsListResponse, extResp.DirContents) | ||||
| 		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) | ||||
| 	t.Run("GetContentsOrList(NoSuchTreePath)", func(t *testing.T) { | ||||
| 		extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "no-such/file.md"}) | ||||
| 		assert.Error(t, err) | ||||
| 		assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]") | ||||
| 		assert.Nil(t, fileContentResponse) | ||||
| 		assert.EqualError(t, err, "object does not exist [id: , rel_path: no-such]") | ||||
| 		assert.Nil(t, extResp.DirContents) | ||||
| 		assert.Nil(t, extResp.FileContents) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("GetBlobBySHA", func(t *testing.T) { | ||||
| 		sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||
| 		ctx.SetPathParam("id", "1") | ||||
| 		ctx.SetPathParam("sha", sha) | ||||
| 		gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha")) | ||||
| 		expectedGBR := &api.GitBlobResponse{ | ||||
| 			Content:  util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"), | ||||
| 			Encoding: util.ToPointer("base64"), | ||||
| 			URL:      "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", | ||||
| 			SHA:      "65f1bf27bc3bf70f64657658635e66094edbcb4d", | ||||
| 			Size:     180, | ||||
| 		} | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, expectedGBR, gbr) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| 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() | ||||
| 
 | ||||
| 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||
| 	ctx.SetPathParam("id", "1") | ||||
| 	ctx.SetPathParam("sha", 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{ | ||||
| 		Content:  util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"), | ||||
| 		Encoding: util.ToPointer("base64"), | ||||
| 		URL:      "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", | ||||
| 		SHA:      "65f1bf27bc3bf70f64657658635e66094edbcb4d", | ||||
| 		Size:     180, | ||||
| 	} | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expectedGBR, gbr) | ||||
| } | ||||
|  | ||||
| @ -19,12 +19,12 @@ import ( | ||||
| 	"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 | ||||
| 	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 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)) | ||||
| 		} | ||||
| 		if size > setting.API.DefaultMaxResponseSize { | ||||
| @ -38,8 +38,8 @@ func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Reposito | ||||
| 	return files | ||||
| } | ||||
| 
 | ||||
| func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) { | ||||
| 	files := GetContentsListFromTreePaths(ctx, repo, refCommit, treeNames) | ||||
| func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) { | ||||
| 	files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames) | ||||
| 	fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil | ||||
| 	verification := GetPayloadCommitVerification(ctx, refCommit.Commit) | ||||
| 	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: 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 { | ||||
| 		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": { | ||||
|       "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 of all the entries of the root dir", | ||||
|         "summary": "Gets the metadata of all the entries of the root dir.", | ||||
|         "operationId": "repoGetContentsList", | ||||
|         "parameters": [ | ||||
|           { | ||||
| @ -7518,15 +7519,72 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/contents/{filepath}": { | ||||
|     "/repos/{owner}/{repo}/contents-ext/{filepath}": { | ||||
|       "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": [ | ||||
|           "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", | ||||
|         "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", | ||||
|         "parameters": [ | ||||
|           { | ||||
| @ -22255,6 +22313,22 @@ | ||||
|       }, | ||||
|       "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": { | ||||
|       "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content", | ||||
|       "type": "object", | ||||
| @ -22298,6 +22372,15 @@ | ||||
|           "format": "date-time", | ||||
|           "x-go-name": "LastCommitterDate" | ||||
|         }, | ||||
|         "lfs_oid": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "LfsOid" | ||||
|         }, | ||||
|         "lfs_size": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "LfsSize" | ||||
|         }, | ||||
|         "name": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Name" | ||||
| @ -24947,6 +25030,15 @@ | ||||
|           "type": "string", | ||||
|           "x-go-name": "Encoding" | ||||
|         }, | ||||
|         "lfs_oid": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "LfsOid" | ||||
|         }, | ||||
|         "lfs_size": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "LfsSize" | ||||
|         }, | ||||
|         "sha": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "SHA" | ||||
| @ -28693,6 +28785,12 @@ | ||||
|         "$ref": "#/definitions/Compare" | ||||
|       } | ||||
|     }, | ||||
|     "ContentsExtResponse": { | ||||
|       "description": "", | ||||
|       "schema": { | ||||
|         "$ref": "#/definitions/ContentsExtResponse" | ||||
|       } | ||||
|     }, | ||||
|     "ContentsListResponse": { | ||||
|       "description": "ContentsListResponse", | ||||
|       "schema": { | ||||
|  | ||||
| @ -7,6 +7,7 @@ import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"slices" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| @ -20,9 +21,9 @@ import ( | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) *api.ContentsResponse { | ||||
| @ -54,7 +55,11 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
| @ -76,20 +81,20 @@ func testAPIGetContents(t *testing.T, u *url.URL) { | ||||
| 
 | ||||
| 	// Get the commit ID of the default branch | ||||
| 	gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1) | ||||
| 	assert.NoError(t, err) | ||||
| 	require.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) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch) | ||||
| 	assert.NoError(t, err) | ||||
| 	require.NoError(t, err) | ||||
| 	// Make a new tag in repo1 | ||||
| 	newTag := "test_tag" | ||||
| 	err = gitRepo.CreateTag(newTag, commitID) | ||||
| 	assert.NoError(t, err) | ||||
| 	require.NoError(t, err) | ||||
| 	/*** END SETUP ***/ | ||||
| 
 | ||||
| 	// ref is default ref | ||||
| @ -99,7 +104,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) { | ||||
| 	resp := MakeRequest(t, req, http.StatusOK) | ||||
| 	var contentsResponse api.ContentsResponse | ||||
| 	DecodeJSON(t, resp, &contentsResponse) | ||||
| 	assert.NotNil(t, contentsResponse) | ||||
| 	lastCommit, _ := gitRepo.GetCommitByPath("README.md") | ||||
| 	expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String()) | ||||
| 	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) | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &contentsResponse) | ||||
| 	assert.NotNil(t, contentsResponse) | ||||
| 	expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String()) | ||||
| 	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) | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &contentsResponse) | ||||
| 	assert.NotNil(t, contentsResponse) | ||||
| 	branchCommit, _ := gitRepo.GetBranchCommit(ref) | ||||
| 	lastCommit, _ = branchCommit.GetCommitByPath("README.md") | ||||
| 	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) | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &contentsResponse) | ||||
| 	assert.NotNil(t, contentsResponse) | ||||
| 	tagCommit, _ := gitRepo.GetTagCommit(ref) | ||||
| 	lastCommit, _ = tagCommit.GetCommitByPath("README.md") | ||||
| 	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) | ||||
| 	resp = MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &contentsResponse) | ||||
| 	assert.NotNil(t, contentsResponse) | ||||
| 	expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID) | ||||
| 	assert.Equal(t, *expectedContentsResponse, contentsResponse) | ||||
| 
 | ||||
| @ -168,9 +168,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { | ||||
| 	MakeRequest(t, req, http.StatusOK) | ||||
| } | ||||
| 
 | ||||
| func TestAPIGetContentsRefFormats(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| func testAPIGetContentsRefFormats(t *testing.T) { | ||||
| 	file := "README.md" | ||||
| 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||
| 	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 | ||||
| 	_ = 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 { | ||||
| 		filename, sha, content string | ||||
| 		size                   int64 | ||||
| 		lfsOid                 *string | ||||
| 		lfsSize                *int64 | ||||
| 	}{ | ||||
| 		{ | ||||
| 			filename: "README.txt", | ||||
| @ -299,6 +301,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str | ||||
| 			sha:      "d4a41a0d4db4949e129bd22f871171ea988103ef", | ||||
| 			size:     129, | ||||
| 			content:  "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6MmVjY2RiNDM4MjVkMmE0OWQ5OWQ1NDJkYWEyMDA3NWNmZjFkOTdkOWQyMzQ5YTg5NzdlZmU5YzAzNjYxNzM3YwpzaXplIDIwNDgK", | ||||
| 			lfsOid:   util.ToPointer("2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c"), | ||||
| 			lfsSize:  util.ToPointer(int64(2048)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			filename: "jpeg.jpeg", | ||||
| @ -311,6 +315,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str | ||||
| 			sha:      "2b6c6c4eaefa24b22f2092c3d54b263ff26feb58", | ||||
| 			size:     127, | ||||
| 			content:  "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6N2I2YjJjODhkYmE5Zjc2MGExYTU4NDY5YjY3ZmVlMmI2OThlZjdlOTM5OWM0Y2E0ZjM0YTE0Y2NiZTM5ZjYyMwpzaXplIDI3Cg==", | ||||
| 			lfsOid:   util.ToPointer("7b6b2c88dba9f760a1a58469b67fee2b698ef7e9399c4ca4f34a14ccbe39f623"), | ||||
| 			lfsSize:  util.ToPointer(int64(27)), | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| @ -339,6 +345,8 @@ func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA str | ||||
| 				GitURL:  &gitURL, | ||||
| 				HTMLURL: &htmlURL, | ||||
| 			}, | ||||
| 			LfsOid:  detail.lfsOid, | ||||
| 			LfsSize: detail.lfsSize, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user