mirror of https://github.com/harness/drone.git
297 lines
7.8 KiB
Go
297 lines
7.8 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/harness/gitness/app/api/controller"
|
|
"github.com/harness/gitness/app/auth"
|
|
"github.com/harness/gitness/gitrpc"
|
|
"github.com/harness/gitness/types"
|
|
"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 << 22 // 4 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 *types.Commit `json:"latest_commit,omitempty"`
|
|
}
|
|
|
|
type GetContentOutput struct {
|
|
ContentInfo
|
|
Content Content `json:"content"`
|
|
}
|
|
|
|
// Content restricts the possible types of content returned by the api.
|
|
type Content interface {
|
|
isContent()
|
|
}
|
|
|
|
type FileContent struct {
|
|
Encoding enum.ContentEncodingType `json:"encoding"`
|
|
Data string `json:"data"`
|
|
Size int64 `json:"size"`
|
|
DataSize int64 `json:"data_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:"commit_sha"`
|
|
}
|
|
|
|
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.
|
|
func (c *Controller) GetContent(ctx context.Context,
|
|
session *auth.Session,
|
|
repoRef string,
|
|
gitRef string,
|
|
repoPath string,
|
|
includeLatestCommit bool,
|
|
) (*GetContentOutput, error) {
|
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set gitRef to default branch in case an empty reference was provided
|
|
if gitRef == "" {
|
|
gitRef = repo.DefaultBranch
|
|
}
|
|
|
|
// create read params once
|
|
readParams := CreateRPCReadParams(repo)
|
|
|
|
treeNodeOutput, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
|
|
ReadParams: readParams,
|
|
GitREF: gitRef,
|
|
Path: repoPath,
|
|
IncludeLatestCommit: includeLatestCommit,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read tree node: %w", err)
|
|
}
|
|
|
|
info, err := mapToContentInfo(treeNodeOutput.Node, treeNodeOutput.Commit, includeLatestCommit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var content Content
|
|
switch info.Type {
|
|
case ContentTypeDir:
|
|
content, err = c.getDirContent(ctx, readParams, gitRef, repoPath, includeLatestCommit)
|
|
case ContentTypeFile:
|
|
content, err = c.getFileContent(ctx, readParams, info.SHA)
|
|
case ContentTypeSymlink:
|
|
content, err = c.getSymlinkContent(ctx, readParams, info.SHA)
|
|
case ContentTypeSubmodule:
|
|
content, err = c.getSubmoduleContent(ctx, readParams, 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,
|
|
readParams gitrpc.ReadParams,
|
|
gitRef string,
|
|
repoPath string,
|
|
commitSHA string,
|
|
) (*SubmoduleContent, error) {
|
|
output, err := c.gitRPCClient.GetSubmodule(ctx, &gitrpc.GetSubmoduleParams{
|
|
ReadParams: readParams,
|
|
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,
|
|
readParams gitrpc.ReadParams,
|
|
blobSHA string,
|
|
) (*FileContent, error) {
|
|
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
|
|
ReadParams: readParams,
|
|
SHA: blobSHA,
|
|
SizeLimit: maxGetContentFileSize,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file content: %w", err)
|
|
}
|
|
|
|
content, err := io.ReadAll(output.Content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read blob content: %w", err)
|
|
}
|
|
|
|
return &FileContent{
|
|
Size: output.Size,
|
|
DataSize: output.ContentSize,
|
|
Encoding: enum.ContentEncodingTypeBase64,
|
|
Data: base64.StdEncoding.EncodeToString(content),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getSymlinkContent(ctx context.Context,
|
|
readParams gitrpc.ReadParams,
|
|
blobSHA string,
|
|
) (*SymlinkContent, error) {
|
|
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
|
|
ReadParams: readParams,
|
|
SHA: blobSHA,
|
|
SizeLimit: maxGetContentFileSize, // TODO: do we need to guard against too big symlinks?
|
|
})
|
|
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)
|
|
}
|
|
|
|
content, err := io.ReadAll(output.Content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read blob content: %w", err)
|
|
}
|
|
|
|
return &SymlinkContent{
|
|
Size: output.Size,
|
|
Target: string(content),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Controller) getDirContent(ctx context.Context,
|
|
readParams gitrpc.ReadParams,
|
|
gitRef string,
|
|
repoPath string,
|
|
includeLatestCommit bool,
|
|
) (*DirContent, error) {
|
|
output, err := c.gitRPCClient.ListTreeNodes(ctx, &gitrpc.ListTreeNodeParams{
|
|
ReadParams: readParams,
|
|
GitREF: gitRef,
|
|
Path: repoPath,
|
|
IncludeLatestCommit: includeLatestCommit,
|
|
})
|
|
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, node := range output.Nodes {
|
|
entries[i], err = mapToContentInfo(node, nil, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &DirContent{
|
|
Entries: entries,
|
|
}, nil
|
|
}
|
|
|
|
func mapToContentInfo(node gitrpc.TreeNode, commit *gitrpc.Commit, includeLatestCommit bool) (ContentInfo, error) {
|
|
typ, err := mapNodeModeToContentType(node.Mode)
|
|
if err != nil {
|
|
return ContentInfo{}, err
|
|
}
|
|
|
|
res := ContentInfo{
|
|
Type: typ,
|
|
SHA: node.SHA,
|
|
Name: node.Name,
|
|
Path: node.Path,
|
|
}
|
|
|
|
// parse commit only if available
|
|
if commit != nil && includeLatestCommit {
|
|
res.LatestCommit, err = controller.MapCommit(commit)
|
|
if err != nil {
|
|
return ContentInfo{}, err
|
|
}
|
|
}
|
|
|
|
return res, 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)
|
|
}
|
|
}
|