mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-07 09:42:05 +00:00
deccd53c24
1. always use "last commit cache" 2. correctly build the cache key for any input (SafeCacheKey) 3. fix the git note "last commit cache FIXME" and avoid OOM
289 lines
9.7 KiB
Go
289 lines
9.7 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package files
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/lfs"
|
|
"gitea.dev/modules/setting"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/routers/api/v1/utils"
|
|
)
|
|
|
|
// ContentType repo content type
|
|
type ContentType string
|
|
|
|
// The string representations of different content types
|
|
const (
|
|
ContentTypeRegular ContentType = "file" // regular content type (file)
|
|
ContentTypeDir ContentType = "dir" // dir content type (dir)
|
|
ContentTypeLink ContentType = "symlink" // link content type (symlink)
|
|
ContentTypeSubmodule ContentType = "submodule" // submodule content type (submodule)
|
|
)
|
|
|
|
type GetContentsOrListOptions struct {
|
|
TreePath string
|
|
IncludeSingleFileContent bool // include the file's content when the tree path is a file
|
|
IncludeLfsMetadata bool
|
|
IncludeCommitMetadata bool
|
|
IncludeCommitMessage 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, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) {
|
|
entry, err := prepareGetContentsEntry(ctx, gitRepo, refCommit, &opts.TreePath)
|
|
if repo.IsEmpty && opts.TreePath == "" {
|
|
return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil
|
|
}
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
// get file contents
|
|
if entry.Type() != "tree" {
|
|
ret.FileContents, err = getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
|
|
return ret, err
|
|
}
|
|
|
|
// list directory contents
|
|
gitTree, err := refCommit.Commit.SubTree(ctx, gitRepo, opts.TreePath)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
entries, err := gitTree.ListEntries(ctx, gitRepo)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
ret.DirContents = make([]*api.ContentsResponse, 0, len(entries))
|
|
for _, e := range entries {
|
|
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 ret, err
|
|
}
|
|
ret.DirContents = append(ret.DirContents, fileContentResponse)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// GetObjectTypeFromTreeEntry check what content is behind it
|
|
func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
|
|
switch {
|
|
case entry.IsDir():
|
|
return ContentTypeDir
|
|
case entry.IsSubModule():
|
|
return ContentTypeSubmodule
|
|
case entry.IsExecutable(), entry.IsRegular():
|
|
return ContentTypeRegular
|
|
case entry.IsLink():
|
|
return ContentTypeLink
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func prepareGetContentsEntry(ctx context.Context, gitRepo *git.Repository, 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
|
|
|
|
// Only allow safe ref types
|
|
refType := refCommit.RefName.RefType()
|
|
if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit {
|
|
return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName)
|
|
}
|
|
|
|
return refCommit.Commit.GetTreeEntryByPath(ctx, gitRepo, *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(ctx, gitRepo, refCommit, &opts.TreePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
|
|
}
|
|
|
|
func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
|
|
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
|
|
}
|
|
selfURLString := selfURL.String()
|
|
|
|
// All content types have these fields in populated
|
|
contentsResponse := &api.ContentsResponse{
|
|
Name: entry.Name(),
|
|
Path: opts.TreePath,
|
|
SHA: entry.ID.String(),
|
|
Mode: entry.Mode().String(),
|
|
Size: entry.GetSize(ctx, gitRepo),
|
|
URL: &selfURLString,
|
|
Links: &api.FileLinksResponse{
|
|
Self: &selfURLString,
|
|
},
|
|
}
|
|
|
|
if opts.IncludeCommitMetadata || opts.IncludeCommitMessage {
|
|
lastCommit, err := refCommit.Commit.GetCommitByPath(ctx, gitRepo, opts.TreePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts.IncludeCommitMetadata {
|
|
contentsResponse.LastCommitSHA = new(lastCommit.ID.String())
|
|
// GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
|
|
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
|
|
if lastCommit.Committer != nil {
|
|
contentsResponse.LastCommitterDate = new(lastCommit.Committer.When)
|
|
}
|
|
if lastCommit.Author != nil {
|
|
contentsResponse.LastAuthorDate = new(lastCommit.Author.When)
|
|
}
|
|
}
|
|
if opts.IncludeCommitMessage {
|
|
contentsResponse.LastCommitMessage = new(lastCommit.MessageUTF8())
|
|
}
|
|
}
|
|
|
|
// Now populate the rest of the ContentsResponse based on the entry type
|
|
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 opts.IncludeSingleFileContent {
|
|
blobResponse, err := GetBlobBySHA(ctx, 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(ctx, gitRepo, entry.ID.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else if entry.IsDir() {
|
|
contentsResponse.Type = string(ContentTypeDir)
|
|
} else if entry.IsLink() {
|
|
contentsResponse.Type = string(ContentTypeLink)
|
|
// The target of a symlink file is the content of the file
|
|
targetFromContent, err := entry.Blob(gitRepo).GetBlobContent(ctx, 1024)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contentsResponse.Target = &targetFromContent
|
|
} else if entry.IsSubModule() {
|
|
contentsResponse.Type = string(ContentTypeSubmodule)
|
|
submodule, err := commit.GetSubModule(ctx, gitRepo, opts.TreePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if submodule != nil && submodule.URL != "" {
|
|
contentsResponse.SubmoduleGitURL = &submodule.URL
|
|
}
|
|
}
|
|
// Handle links
|
|
if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
|
|
downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
downloadURLString := downloadURL.String()
|
|
contentsResponse.DownloadURL = &downloadURLString
|
|
}
|
|
if !entry.IsSubModule() {
|
|
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
htmlURLString := htmlURL.String()
|
|
contentsResponse.HTMLURL = &htmlURLString
|
|
contentsResponse.Links.HTMLURL = &htmlURLString
|
|
|
|
gitURL, err := url.Parse(repo.APIURL() + "/git/blobs/" + url.PathEscape(entry.ID.String()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gitURLString := gitURL.String()
|
|
contentsResponse.GitURL = &gitURLString
|
|
contentsResponse.Links.GitURL = &gitURLString
|
|
}
|
|
|
|
return contentsResponse, nil
|
|
}
|
|
|
|
func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
|
|
gitBlob, err := gitRepo.GetBlob(sha)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret := &api.GitBlobResponse{
|
|
SHA: gitBlob.ID.String(),
|
|
URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
|
|
Size: gitBlob.Size(ctx),
|
|
}
|
|
|
|
blobSize := gitBlob.Size(ctx)
|
|
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(ctx, originContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret.Encoding, ret.Content = new("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(ctx context.Context, gitRepo *git.Repository, sha string) (*string, *int64, error) {
|
|
gitBlob, err := gitRepo.GetBlob(sha)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if gitBlob.Size(ctx) > lfs.MetaFileMaxSize {
|
|
return nil, nil, nil // not a LFS pointer
|
|
}
|
|
buf, err := gitBlob.GetBlobContent(ctx, lfs.MetaFileMaxSize)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
oid, size := parsePossibleLfsPointerBuffer(strings.NewReader(buf))
|
|
return oid, size, nil
|
|
}
|