drone/git/adapter/tree.go

241 lines
6.6 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 adapter
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"path"
"regexp"
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/types"
gitea "code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log"
)
func cleanTreePath(treePath string) string {
return strings.Trim(path.Clean("/"+treePath), "/")
}
func parseTreeNodeMode(s string) (types.TreeNodeType, types.TreeNodeMode, error) {
switch s {
case "100644":
return types.TreeNodeTypeBlob, types.TreeNodeModeFile, nil
case "120000":
return types.TreeNodeTypeBlob, types.TreeNodeModeSymlink, nil
case "100755":
return types.TreeNodeTypeBlob, types.TreeNodeModeExec, nil
case "160000":
return types.TreeNodeTypeCommit, types.TreeNodeModeCommit, nil
case "040000":
return types.TreeNodeTypeTree, types.TreeNodeModeTree, nil
default:
return types.TreeNodeTypeBlob, types.TreeNodeModeFile,
fmt.Errorf("unknown git tree node mode: '%s'", s)
}
}
func scanZeroSeparated(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil // Return nothing if at end of file and no data passed
}
if i := strings.IndexByte(string(data), 0); i >= 0 {
return i + 1, data[0:i], nil // Split at zero byte
}
if atEOF {
return len(data), data, nil // at the end of file return the data
}
return
}
// regexpLsTreeColumns is a regular expression that is used to parse a single line
// of a "git ls-tree" output (which uses the NULL character as the line break).
// The single line mode must be used because output might contain the EOL and other control characters.
var regexpLsTreeColumns = regexp.MustCompile(`(?s)^(\d{6})\s+(\w+)\s+(\w+)\t(.+)`)
func lsTree(
ctx context.Context,
repoPath string,
rev string,
treePath string,
) ([]types.TreeNode, error) {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
args := []string{"ls-tree", "-z", rev, treePath}
output, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath})
if strings.Contains(stderr, "fatal: Not a valid object name") {
return nil, errors.NotFound("revision %q not found", rev)
}
if err != nil {
return nil, fmt.Errorf("failed to run git ls-tree: %w", err)
}
if output == "" {
return nil, &types.PathNotFoundError{Path: treePath}
}
n := strings.Count(output, "\x00")
list := make([]types.TreeNode, 0, n)
scan := bufio.NewScanner(strings.NewReader(output))
scan.Split(scanZeroSeparated)
for scan.Scan() {
line := scan.Text()
columns := regexpLsTreeColumns.FindStringSubmatch(line)
if columns == nil {
log.Ctx(ctx).Error().
Str("ls-tree", output). // logs the whole directory listing for the additional context
Str("line", line).
Msg("unrecognized format of git directory listing")
return nil, fmt.Errorf("unrecognized format of git directory listing: %q", line)
}
nodeType, nodeMode, err := parseTreeNodeMode(columns[1])
if err != nil {
log.Ctx(ctx).Err(err).
Str("line", line).
Msg("failed to parse git mode")
return nil, fmt.Errorf("failed to parse git node type and file mode: %w", err)
}
nodeSha := columns[3]
nodePath := columns[4]
nodeName := path.Base(nodePath)
list = append(list, types.TreeNode{
NodeType: nodeType,
Mode: nodeMode,
Sha: nodeSha,
Name: nodeName,
Path: nodePath,
})
}
return list, nil
}
// lsFile returns all tree node entries in the requested directory.
func lsDirectory(
ctx context.Context,
repoPath string,
rev string,
treePath string,
) ([]types.TreeNode, error) {
treePath = path.Clean(treePath)
if treePath == "" {
treePath = "."
} else {
treePath += "/"
}
return lsTree(ctx, repoPath, rev, treePath)
}
// lsFile returns one tree node entry.
func lsFile(
ctx context.Context,
repoPath string,
rev string,
treePath string,
) (types.TreeNode, error) {
treePath = cleanTreePath(treePath)
list, err := lsTree(ctx, repoPath, rev, treePath)
if err != nil {
return types.TreeNode{}, fmt.Errorf("failed to ls file: %w", err)
}
if len(list) != 1 {
return types.TreeNode{}, fmt.Errorf("ls file list contains more than one element, len=%d", len(list))
}
return list[0], nil
}
// GetTreeNode returns the tree node at the given path as found for the provided reference.
func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*types.TreeNode, error) {
// root path (empty path) is a special case
if treePath == "" {
if repoPath == "" {
return nil, ErrRepositoryPathEmpty
}
args := []string{"show", "--no-patch", "--format=" + fmtTreeHash, rev}
treeSHA, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath})
if strings.Contains(stderr, "ambiguous argument") {
return nil, errors.NotFound("could not resolve git revision: %s", rev)
}
if err != nil {
return nil, fmt.Errorf("failed to get root tree node: %w", err)
}
return &types.TreeNode{
NodeType: types.TreeNodeTypeTree,
Mode: types.TreeNodeModeTree,
Sha: treeSHA,
Name: "",
Path: "",
}, err
}
treeNode, err := lsFile(ctx, repoPath, rev, treePath)
if err != nil {
return nil, fmt.Errorf("failed to get tree node: %w", err)
}
return &treeNode, nil
}
// ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path.
func (a Adapter) ListTreeNodes(ctx context.Context, repoPath, rev, treePath string) ([]types.TreeNode, error) {
list, err := lsDirectory(ctx, repoPath, rev, treePath)
if err != nil {
return nil, fmt.Errorf("failed to list tree nodes: %w", err)
}
return list, nil
}
func (a Adapter) ReadTree(
ctx context.Context,
repoPath string,
ref string,
w io.Writer,
args ...string,
) error {
if repoPath == "" {
return ErrRepositoryPathEmpty
}
errbuf := bytes.Buffer{}
if err := gitea.NewCommand(ctx, append([]string{"read-tree", ref}, args...)...).
Run(&gitea.RunOpts{
Dir: repoPath,
Stdout: w,
Stderr: &errbuf,
}); err != nil {
return fmt.Errorf("unable to read %s in to the index: %w\n%s",
ref, err, errbuf.String())
}
return nil
}