mirror of https://github.com/harness/drone.git
334 lines
8.5 KiB
Go
334 lines
8.5 KiB
Go
// Copyright 2022 Harness Inc. All rights reserved.
|
|
// Use of this source code is governed by the Polyform Free Trial License
|
|
// that can be found in the LICENSE.md file for this repository.
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/gitrpc"
|
|
apiauth "github.com/harness/gitness/internal/api/auth"
|
|
"github.com/harness/gitness/internal/auth"
|
|
"github.com/harness/gitness/types/enum"
|
|
)
|
|
|
|
const (
|
|
// maxGetContentFileSize specifies the maximum number of bytes a file content response contains.
|
|
// If a file is any larger, the content is truncated.
|
|
maxGetContentFileSize = 1 << 27 // 128 MB
|
|
)
|
|
|
|
type ContentType string
|
|
|
|
const (
|
|
ContentTypeFile ContentType = "file"
|
|
ContentTypeDir ContentType = "dir"
|
|
ContentTypeSymlink ContentType = "symlink"
|
|
ContentTypeSubmodule ContentType = "submodule"
|
|
)
|
|
|
|
type ContentInfo struct {
|
|
Type ContentType `json:"type"`
|
|
SHA string `json:"sha"`
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
LatestCommit *Commit `json:"latestCommit,omitempty"`
|
|
}
|
|
|
|
type Commit struct {
|
|
SHA string `json:"sha"`
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
Author Signature `json:"author"`
|
|
Committer Signature `json:"committer"`
|
|
}
|
|
|
|
type Signature struct {
|
|
Identity Identity `json:"identity"`
|
|
When time.Time `json:"when"`
|
|
}
|
|
|
|
type Identity struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type GetContentOutput struct {
|
|
ContentInfo
|
|
Content Content `json:"content"`
|
|
}
|
|
|
|
type FileEncodingType string
|
|
|
|
const (
|
|
FileEncodingTypeBase64 FileEncodingType = "base64"
|
|
)
|
|
|
|
// Content restricts the possible types of content returned by the api.
|
|
type Content interface {
|
|
isContent()
|
|
}
|
|
|
|
type FileContent struct {
|
|
Encoding FileEncodingType `json:"encoding"`
|
|
Data string `json:"data"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func (c *FileContent) isContent() {}
|
|
|
|
type SymlinkContent struct {
|
|
Target string `json:"target"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func (c *SymlinkContent) isContent() {}
|
|
|
|
type DirContent struct {
|
|
Entries []ContentInfo `json:"entries"`
|
|
}
|
|
|
|
func (c *DirContent) isContent() {}
|
|
|
|
type SubmoduleContent struct {
|
|
URL string `json:"url"`
|
|
CommitSHA string `json:"commitSha"`
|
|
}
|
|
|
|
func (c *SubmoduleContent) isContent() {}
|
|
|
|
/*
|
|
* GetContent finds the content of the repo at the given path.
|
|
* If no gitRef is provided, the content is retrieved from the default branch.
|
|
* If includeLatestCommit is enabled, the response contains information of the latest commit that changed the object.
|
|
*/
|
|
func (c *Controller) GetContent(ctx context.Context, session *auth.Session, repoRef string,
|
|
gitRef string, repoPath string, includeLatestCommit bool) (*GetContentOutput, error) {
|
|
repo, err := c.repoStore.FindRepoFromRef(ctx, repoRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, enum.PermissionRepoView, true); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set gitRef to default branch in case an empty reference was provided
|
|
if gitRef == "" {
|
|
gitRef = repo.DefaultBranch
|
|
}
|
|
|
|
treeNodeOutput, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
|
|
RepoUID: repo.GitUID,
|
|
GitREF: gitRef,
|
|
Path: repoPath,
|
|
IncludeLatestCommit: includeLatestCommit,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info, err := mapToContentInfo(&treeNodeOutput.Node, treeNodeOutput.Commit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var content Content
|
|
switch info.Type {
|
|
case ContentTypeDir:
|
|
// for getContent we don't want any recursiveness for dir content.
|
|
content, err = c.getDirContent(ctx, repo.GitUID, gitRef, repoPath, includeLatestCommit, false)
|
|
case ContentTypeFile:
|
|
content, err = c.getFileContent(ctx, repo.GitUID, info.SHA)
|
|
case ContentTypeSymlink:
|
|
content, err = c.getSymlinkContent(ctx, repo.GitUID, info.SHA)
|
|
case ContentTypeSubmodule:
|
|
content, err = c.getSubmoduleContent(ctx, repo.GitUID, gitRef, repoPath, info.SHA)
|
|
default:
|
|
err = fmt.Errorf("unknown tree node type '%s'", treeNodeOutput.Node.Type)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &GetContentOutput{
|
|
ContentInfo: *info,
|
|
Content: content,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getSubmoduleContent(ctx context.Context, gitRepoUID string, gitRef string,
|
|
repoPath string, commitSHA string) (*SubmoduleContent, error) {
|
|
output, err := c.gitRPCClient.GetSubmodule(ctx, &gitrpc.GetSubmoduleParams{
|
|
RepoUID: gitRepoUID,
|
|
GitREF: gitRef,
|
|
Path: repoPath,
|
|
})
|
|
if err != nil {
|
|
// TODO: handle not found error
|
|
// This requires gitrpc to also return notfound though!
|
|
return nil, fmt.Errorf("failed to get submodule: %w", err)
|
|
}
|
|
|
|
return &SubmoduleContent{
|
|
URL: output.Submodule.URL,
|
|
CommitSHA: commitSHA,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getFileContent(ctx context.Context, gitRepoUID string, blobSHA string) (*FileContent, error) {
|
|
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
|
|
RepoUID: gitRepoUID,
|
|
SHA: blobSHA,
|
|
SizeLimit: maxGetContentFileSize,
|
|
})
|
|
if err != nil {
|
|
// TODO: handle not found error
|
|
// This requires gitrpc to also return notfound though!
|
|
return nil, fmt.Errorf("failed to get file content: %w", err)
|
|
}
|
|
|
|
return &FileContent{
|
|
Size: output.Blob.Size,
|
|
Encoding: FileEncodingTypeBase64,
|
|
Data: base64.StdEncoding.EncodeToString(output.Blob.Content),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getSymlinkContent(ctx context.Context, gitRepoUID string,
|
|
blobSHA string) (*SymlinkContent, error) {
|
|
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
|
|
RepoUID: gitRepoUID,
|
|
SHA: blobSHA,
|
|
SizeLimit: maxGetContentFileSize,
|
|
})
|
|
if err != nil {
|
|
// TODO: handle not found error
|
|
// This requires gitrpc to also return notfound though!
|
|
return nil, fmt.Errorf("failed to get symlink: %w", err)
|
|
}
|
|
|
|
return &SymlinkContent{
|
|
Size: output.Blob.Size,
|
|
Target: string(output.Blob.Content),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getDirContent(ctx context.Context, gitRepoUID string, gitRef string,
|
|
repoPath string, includeLatestCommit bool, recursive bool) (*DirContent, error) {
|
|
output, err := c.gitRPCClient.ListTreeNodes(ctx, &gitrpc.ListTreeNodeParams{
|
|
RepoUID: gitRepoUID,
|
|
GitREF: gitRef,
|
|
Path: repoPath,
|
|
IncludeLatestCommit: includeLatestCommit,
|
|
Recursive: recursive,
|
|
})
|
|
if err != nil {
|
|
// TODO: handle not found error
|
|
// This requires gitrpc to also return notfound though!
|
|
return nil, fmt.Errorf("failed to get content of dir: %w", err)
|
|
}
|
|
|
|
entries := make([]ContentInfo, len(output.Nodes))
|
|
for i := range output.Nodes {
|
|
node := output.Nodes[i]
|
|
|
|
var entry *ContentInfo
|
|
entry, err = mapToContentInfo(&node.TreeNode, node.Commit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries[i] = *entry
|
|
}
|
|
|
|
return &DirContent{
|
|
Entries: entries,
|
|
}, nil
|
|
}
|
|
|
|
func mapToContentInfo(node *gitrpc.TreeNode, commit *gitrpc.Commit) (*ContentInfo, error) {
|
|
// node data is expected
|
|
if node == nil {
|
|
return nil, fmt.Errorf("node can't be nil")
|
|
}
|
|
typ, err := mapNodeModeToContentType(node.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &ContentInfo{
|
|
Type: typ,
|
|
SHA: node.SHA,
|
|
Name: node.Name,
|
|
Path: node.Path,
|
|
}
|
|
|
|
// parse commit only if available
|
|
if commit != nil {
|
|
res.LatestCommit, err = mapCommit(commit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func mapCommit(c *gitrpc.Commit) (*Commit, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("commit is nil")
|
|
}
|
|
|
|
author, err := mapSignature(&c.Author)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map author: %w", err)
|
|
}
|
|
|
|
committer, err := mapSignature(&c.Committer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to map committer: %w", err)
|
|
}
|
|
|
|
return &Commit{
|
|
SHA: c.SHA,
|
|
Title: c.Title,
|
|
Message: c.Message,
|
|
Author: *author,
|
|
Committer: *committer,
|
|
}, nil
|
|
}
|
|
|
|
func mapSignature(s *gitrpc.Signature) (*Signature, error) {
|
|
if s == nil {
|
|
return nil, fmt.Errorf("signature is nil")
|
|
}
|
|
|
|
return &Signature{
|
|
Identity: Identity{
|
|
Name: s.Identity.Name,
|
|
Email: s.Identity.Email,
|
|
},
|
|
When: s.When,
|
|
}, nil
|
|
}
|
|
|
|
func mapNodeModeToContentType(m gitrpc.TreeNodeMode) (ContentType, error) {
|
|
switch m {
|
|
case gitrpc.TreeNodeModeFile, gitrpc.TreeNodeModeExec:
|
|
return ContentTypeFile, nil
|
|
case gitrpc.TreeNodeModeSymlink:
|
|
return ContentTypeSymlink, nil
|
|
case gitrpc.TreeNodeModeCommit:
|
|
return ContentTypeSubmodule, nil
|
|
case gitrpc.TreeNodeModeTree:
|
|
return ContentTypeDir, nil
|
|
default:
|
|
return ContentTypeFile, fmt.Errorf("unsupported tree node mode '%s'", m)
|
|
}
|
|
}
|