drone/internal/gitrpc/gitea.go

919 lines
26 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 gitrpc
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math"
"path"
"path/filepath"
"strconv"
"strings"
"time"
gitea "code.gitea.io/gitea/modules/git"
gitearef "code.gitea.io/gitea/modules/git/foreachref"
)
const (
giteaPrettyLogFormat = `--pretty=format:%H`
)
type giteaAdapter struct {
}
func newGiteaAdapter() (giteaAdapter, error) {
err := gitea.InitSimple(context.Background())
if err != nil {
return giteaAdapter{}, err
}
return giteaAdapter{}, nil
}
// InitRepository initializes a new Git repository.
func (g giteaAdapter) InitRepository(ctx context.Context, repoPath string, bare bool) error {
return gitea.InitRepository(ctx, repoPath, bare)
}
// SetDefaultBranch sets the default branch of a repo.
func (g giteaAdapter) SetDefaultBranch(ctx context.Context, repoPath string,
defaultBranch string, allowEmpty bool) error {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return err
}
defer giteaRepo.Close()
// if requested, error out if branch doesn't exist. Otherwise, blindly set it.
if !allowEmpty && !giteaRepo.IsBranchExist(defaultBranch) {
// TODO: ensure this returns not found error to caller
return fmt.Errorf("branch '%s' does not exist", defaultBranch)
}
// change default branch
err = giteaRepo.SetDefaultBranch(defaultBranch)
if err != nil {
return fmt.Errorf("failed to set new default branch: %w", err)
}
return nil
}
func (g giteaAdapter) Clone(ctx context.Context, from, to string, opts cloneRepoOptions) error {
return gitea.Clone(ctx, from, to, gitea.CloneRepoOptions{
Timeout: opts.timeout,
Mirror: opts.mirror,
Bare: opts.bare,
Quiet: opts.quiet,
Branch: opts.branch,
Shared: opts.shared,
NoCheckout: opts.noCheckout,
Depth: opts.depth,
Filter: opts.filter,
SkipTLSVerify: opts.skipTLSVerify,
})
}
func (g giteaAdapter) AddFiles(repoPath string, all bool, files ...string) error {
return gitea.AddChanges(repoPath, all, files...)
}
func (g giteaAdapter) Commit(repoPath string, opts commitChangesOptions) error {
return gitea.CommitChanges(repoPath, gitea.CommitChangesOptions{
Committer: &gitea.Signature{
Name: opts.committer.identity.name,
Email: opts.committer.identity.email,
When: opts.committer.when,
},
Author: &gitea.Signature{
Name: opts.author.identity.name,
Email: opts.author.identity.email,
When: opts.author.when,
},
Message: opts.message,
})
}
func (g giteaAdapter) Push(ctx context.Context, repoPath string, opts pushOptions) error {
return gitea.Push(ctx, repoPath, gitea.PushOptions{
Remote: opts.remote,
Branch: opts.branch,
Force: opts.force,
Mirror: opts.mirror,
Env: opts.env,
Timeout: opts.timeout,
})
}
func cleanTreePath(treePath string) string {
return strings.Trim(path.Clean("/"+treePath), "/")
}
// GetTreeNode returns the tree node at the given path as found for the provided reference.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) GetTreeNode(ctx context.Context, repoPath string,
ref string, treePath string) (*treeNode, error) {
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
// Get the giteaCommit object for the ref
giteaCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, fmt.Errorf("error getting commit for ref '%s': %w", ref, err)
}
// TODO: handle ErrNotExist :)
giteaTreeEntry, err := giteaCommit.GetTreeEntryByPath(treePath)
if err != nil {
return nil, err
}
nodeType, mode, err := mapGiteaNodeToTreeNodeModeAndType(giteaTreeEntry.Mode())
if err != nil {
return nil, err
}
return &treeNode{
mode: mode,
nodeType: nodeType,
sha: giteaTreeEntry.ID.String(),
name: giteaTreeEntry.Name(),
path: treePath,
}, nil
}
// GetLatestCommit gets the latest commit of a path relative from the provided reference.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) GetLatestCommit(ctx context.Context, repoPath string,
ref string, treePath string) (*commit, error) {
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
giteaCommit, err := giteaGetCommitByPath(giteaRepo, ref, treePath)
if err != nil {
return nil, fmt.Errorf("error getting latest commit for '%s': %w", treePath, err)
}
return mapGiteaCommit(giteaCommit)
}
// giteaGetCommitByPath is a copy of gitea code - required as we want latest commit per specific branch.
func giteaGetCommitByPath(giteaRepo *gitea.Repository, ref string, treePath string) (*gitea.Commit, error) {
if treePath == "" {
treePath = "."
}
// NOTE: the difference to gitea implementation is passing `ref`.
stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, "log", ref, "-1", giteaPrettyLogFormat, "--", treePath).
RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path})
if runErr != nil {
return nil, runErr
}
giteaCommits, err := giteaParsePrettyFormatLogToList(giteaRepo, stdout)
if err != nil {
return nil, err
}
return giteaCommits[0], nil
}
// giteaParsePrettyFormatLogToList is an exact copy of gitea code.
func giteaParsePrettyFormatLogToList(giteaRepo *gitea.Repository, logs []byte) ([]*gitea.Commit, error) {
var giteaCommits []*gitea.Commit
if len(logs) == 0 {
return giteaCommits, nil
}
parts := bytes.Split(logs, []byte{'\n'})
for _, commitID := range parts {
commit, err := giteaRepo.GetCommit(string(commitID))
if err != nil {
return nil, err
}
giteaCommits = append(giteaCommits, commit)
}
return giteaCommits, nil
}
// ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path
// and includes the latest commit for all nodes if requested.
// IMPORTANT: recursive and includeLatestCommit can't be used together.
// Note: ref can be Branch / Tag / CommitSHA.
//
//nolint:gocognit // refactor if needed
func (g giteaAdapter) ListTreeNodes(ctx context.Context, repoPath string,
ref string, treePath string, recursive bool, includeLatestCommit bool) ([]treeNodeWithCommit, error) {
if recursive && includeLatestCommit {
// To avoid potential performance catastrophies, block recursive with includeLatestCommit
// TODO: this should return bad error to caller if needed?
// TODO: should this be refactored in two methods?
return nil, fmt.Errorf("latest commit with recursive query is not supported")
}
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
// Get the giteaCommit object for the ref
giteaCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, fmt.Errorf("error getting commit for ref '%s': %w", ref, err)
}
// Get the giteaTree object for the ref
giteaTree, err := giteaCommit.SubTree(treePath)
if err != nil {
return nil, fmt.Errorf("error getting tree for '%s': %w", treePath, err)
}
var giteaEntries gitea.Entries
if recursive {
giteaEntries, err = giteaTree.ListEntriesRecursive()
} else {
giteaEntries, err = giteaTree.ListEntries()
}
if err != nil {
return nil, fmt.Errorf("failed to list entries for tree '%s': %w", treePath, err)
}
var latestCommits []gitea.CommitInfo
if includeLatestCommit {
// TODO: can be speed up with latestCommitCache (currently nil)
latestCommits, _, err = giteaEntries.GetCommitsInfo(ctx, giteaCommit, treePath, nil)
if err != nil {
return nil, fmt.Errorf("failed to get latest commits for entries: %w", err)
}
if len(latestCommits) != len(giteaEntries) {
return nil, fmt.Errorf("latest commit info doesn't match tree node info - count differs")
}
}
nodes := make([]treeNodeWithCommit, len(giteaEntries))
for i := range giteaEntries {
giteaEntry := giteaEntries[i]
var nodeType treeNodeType
var mode treeNodeMode
nodeType, mode, err = mapGiteaNodeToTreeNodeModeAndType(giteaEntry.Mode())
if err != nil {
return nil, err
}
// giteaNode.Name() returns the path of the node relative to the tree.
relPath := giteaEntry.Name()
name := filepath.Base(relPath)
var commit *commit
if includeLatestCommit {
commit, err = mapGiteaCommit(latestCommits[i].Commit)
if err != nil {
return nil, err
}
}
nodes[i] = treeNodeWithCommit{
treeNode: treeNode{
nodeType: nodeType,
mode: mode,
sha: giteaEntry.ID.String(),
name: name,
path: filepath.Join(treePath, relPath),
},
commit: commit,
}
}
return nodes, nil
}
// ListCommits lists the commits reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) ListCommits(ctx context.Context, repoPath string,
ref string, page int, pageSize int) ([]commit, int64, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, 0, err
}
defer giteaRepo.Close()
// Get the giteaTopCommit object for the ref
giteaTopCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, 0, fmt.Errorf("error getting commit for ref '%s': %w", ref, err)
}
giteaCommits, err := giteaTopCommit.CommitsByRange(page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("error getting commits: %w", err)
}
totalCount, err := giteaTopCommit.CommitsCount()
if err != nil {
return nil, 0, fmt.Errorf("error getting total commit count: %w", err)
}
commits := make([]commit, len(giteaCommits))
for i := range giteaCommits {
var commit *commit
commit, err = mapGiteaCommit(giteaCommits[i])
if err != nil {
return nil, 0, err
}
commits[i] = *commit
}
// TODO: save to cast to int from int64, or we expect exceeding int.MaxValue?
return commits, totalCount, nil
}
// GetCommit returns the (latest) commit for a specific ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) GetCommit(ctx context.Context, repoPath string, ref string) (*commit, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
commit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, err
}
return mapGiteaCommit(commit)
}
// GetCommits returns the (latest) commits for a specific list of refs.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) GetCommits(ctx context.Context, repoPath string, refs []string) ([]commit, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
commits := make([]commit, len(refs))
for i, sha := range refs {
var giteaCommit *gitea.Commit
giteaCommit, err = giteaRepo.GetCommit(sha)
if err != nil {
return nil, err
}
var commit *commit
commit, err = mapGiteaCommit(giteaCommit)
if err != nil {
return nil, err
}
commits[i] = *commit
}
return commits, nil
}
// GetAnnotatedTag returns the tag for a specific tag sha.
func (g giteaAdapter) GetAnnotatedTag(ctx context.Context, repoPath string, sha string) (*tag, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
return giteaGetAnnotatedTag(giteaRepo, sha)
}
// GetAnnotatedTags returns the tags for a specific list of tag sha.
func (g giteaAdapter) GetAnnotatedTags(ctx context.Context, repoPath string, shas []string) ([]tag, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
tags := make([]tag, len(shas))
for i, sha := range shas {
var tag *tag
tag, err = giteaGetAnnotatedTag(giteaRepo, sha)
if err != nil {
return nil, err
}
tags[i] = *tag
}
return tags, nil
}
// giteaGetAnnotatedTag is a custom implementation to retrieve an annotated tag from a sha.
// The code is following parts of the gitea implementation.
//
// IMPORTANT: This is required as all gitea implementations of form get*Tag
// are having huge performance issues (with 2,500 tags it took seconds per single tag!)
func giteaGetAnnotatedTag(giteaRepo *gitea.Repository, sha string) (*tag, error) {
// The tag is an annotated tag with a message.
writer, reader, cancel := giteaRepo.CatFileBatch(giteaRepo.Ctx)
defer cancel()
if _, err := writer.Write([]byte(sha + "\n")); err != nil {
return nil, err
}
tagSha, typ, size, err := gitea.ReadBatchLine(reader)
if err != nil {
if errors.Is(err, io.EOF) || gitea.IsErrNotExist(err) {
return nil, fmt.Errorf("tag with sha %s does not exist", sha)
}
return nil, err
}
if typ != string(gitObjectTypeTag) {
return nil, fmt.Errorf("git object is of type '%s', expected tag", typ)
}
// read the remaining rawData
rawData, err := io.ReadAll(io.LimitReader(reader, size))
if err != nil {
return nil, err
}
_, err = reader.Discard(1)
if err != nil {
return nil, err
}
tag, err := parseTagDataFromCatFile(rawData)
if err != nil {
return nil, fmt.Errorf("failed to parse tag '%s': %w", sha, err)
}
// fill in the sha
tag.sha = string(tagSha)
return tag, nil
}
const (
pgpSignatureBeginToken = "\n-----BEGIN PGP SIGNATURE-----\n" //#nosec G101
pgpSignatureEndToken = "\n-----END PGP SIGNATURE-----" //#nosec G101
)
// parseTagDataFromCatFile parses a tag from a cat-file output.
func parseTagDataFromCatFile(data []byte) (*tag, error) {
tag := &tag{}
p := 0
var err error
// parse object Id
tag.targetSha, p, err = giteaParseCatFileLine(data, p, "object")
if err != nil {
return nil, err
}
// parse object type
rawType, p, err := giteaParseCatFileLine(data, p, "type")
if err != nil {
return nil, err
}
tag.targetType, err = parseGitObjectType(rawType)
if err != nil {
return nil, err
}
// parse tag name
tag.name, p, err = giteaParseCatFileLine(data, p, "tag")
if err != nil {
return nil, err
}
// parse tagger
rawTaggerInfo, p, err := giteaParseCatFileLine(data, p, "tagger")
if err != nil {
return nil, err
}
tag.tagger, err = parseSignatureFromCatFileLine(rawTaggerInfo)
if err != nil {
return nil, err
}
// remainder is message and gpg (remove leading and tailing new lines)
message := string(bytes.Trim(data[p:], "\n"))
// handle gpg signature
pgpEnd := strings.Index(message, pgpSignatureEndToken)
if pgpEnd > -1 {
messageStart := pgpEnd + len(pgpSignatureEndToken)
// for now we just remove the signature (and trim any separating new lines)
// TODO: add support for GPG signature of tags
message = strings.TrimLeft(message[messageStart:], "\n")
}
tag.message = message
// get title from message
tag.title = message
titleEnd := strings.IndexByte(message, '\n')
if titleEnd > -1 {
tag.title = message[:titleEnd]
}
return tag, nil
}
func giteaParseCatFileLine(data []byte, start int, header string) (string, int, error) {
// for simplicity only look at data from start onwards
data = data[start:]
lenHeader := len(header)
lenData := len(data)
if lenData < lenHeader {
return "", 0, fmt.Errorf("expected '%s' but line only contains '%s'", header, string(data))
}
if string(data[:lenHeader]) != header {
return "", 0, fmt.Errorf("expected '%s' but started with '%s'", header, string(data[:lenHeader]))
}
// get end of line and start of next line (used externaly, transpose with provided start index)
lineEnd := bytes.IndexByte(data, '\n')
externalNextLine := start + lineEnd + 1
if lineEnd == -1 {
lineEnd = lenData
externalNextLine = start + lenData
}
// if there's no data, return an error (have to consider for ' ')
if lineEnd <= lenHeader+1 {
return "", 0, fmt.Errorf("no data for line of type '%s'", header)
}
return string(data[lenHeader+1 : lineEnd]), externalNextLine, nil
}
// defaultGitTimeLayout is the (default) time format printed by git.
const defaultGitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700"
// parseSignatureFromCatFileLine parses the signature from a cat-file output.
// This is used for commit / tag outputs. Input will be similar to (without 'author 'prefix):
// - author Max Mustermann <mm@gitness.io> 1666401234 -0700
// - author Max Mustermann <mm@gitness.io> Tue Oct 18 05:13:26 2022 +0530
// TODO: method is leaning on gitea code - requires reference?
func parseSignatureFromCatFileLine(line string) (signature, error) {
sig := signature{}
emailStart := strings.LastIndexByte(line, '<')
emailEnd := strings.LastIndexByte(line, '>')
if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart {
return signature{}, fmt.Errorf("signature is missing email ('%s')", line)
}
// name requires that there is at least one char followed by a space (so emailStart >= 2)
if emailStart < 2 {
return signature{}, fmt.Errorf("signature is missing name ('%s')", line)
}
sig.identity.name = line[:emailStart-1]
sig.identity.email = line[emailStart+1 : emailEnd]
timeStart := emailEnd + 2
if timeStart >= len(line) {
return signature{}, fmt.Errorf("signature is missing time ('%s')", line)
}
// Check if time format is written date time format (e.g Thu, 07 Apr 2005 22:13:13 +0200)
// we can check that by ensuring that the date time part starts with a non-digit character.
if line[timeStart] > '9' {
var err error
sig.when, err = time.Parse(defaultGitTimeLayout, line[timeStart:])
if err != nil {
return signature{}, fmt.Errorf("failed to time.parse signature time ('%s'): %w", line, err)
}
return sig, nil
}
// Otherwise we have to manually parse unix time and time zone
endOfUnixTime := timeStart + strings.IndexByte(line[timeStart:], ' ')
if endOfUnixTime <= timeStart {
return signature{}, fmt.Errorf("signature is missing unix time ('%s')", line)
}
unixSeconds, err := strconv.ParseInt(line[timeStart:endOfUnixTime], 10, 64)
if err != nil {
return signature{}, fmt.Errorf("failed to parse unix time ('%s'): %w", line, err)
}
// parse time zone
startOfTimeZone := endOfUnixTime + 1 // +1 for space
endOfTimeZone := startOfTimeZone + 5 // +5 for '+0700'
if startOfTimeZone >= len(line) || endOfTimeZone > len(line) {
return signature{}, fmt.Errorf("signature is missing time zone ('%s')", line)
}
// get and disect timezone, e.g. '+0700'
rawTimeZone := line[startOfTimeZone:endOfTimeZone]
rawTimeZoneH := rawTimeZone[1:3] // gets +[07]00
rawTimeZoneMin := rawTimeZone[3:] // gets +07[00]
timeZoneH, err := strconv.ParseInt(rawTimeZoneH, 10, 64)
if err != nil {
return signature{}, fmt.Errorf("failed to parse hours of time zone ('%s'): %w", line, err)
}
timeZoneMin, err := strconv.ParseInt(rawTimeZoneMin, 10, 64)
if err != nil {
return signature{}, fmt.Errorf("failed to parse minutes of time zone ('%s'): %w", line, err)
}
timeZoneOffsetInSec := int(timeZoneH*60+timeZoneMin) * 60
if rawTimeZone[0] == '-' {
timeZoneOffsetInSec *= -1
}
timeZone := time.FixedZone("", timeZoneOffsetInSec)
// create final time using unix and timezone translation
sig.when = time.Unix(unixSeconds, 0).In(timeZone)
return sig, nil
}
func defaultInstructor(_ walkReferencesEntry) (walkInstruction, error) {
return walkInstructionHandle, nil
}
// WalkReferences uses the provided options to filter the available references of the repo,
// and calls the handle function for every matching node.
// The instructor & handler are called with a map that contains the matching value for every field provided in fields.
// TODO: walkGiteaReferences related code should be moved to separate file.
func (g giteaAdapter) WalkReferences(ctx context.Context,
repoPath string, handler walkReferencesHandler, opts *walkReferencesOptions) error {
// backfil optional options
if opts.instructor == nil {
opts.instructor = defaultInstructor
}
if len(opts.fields) == 0 {
opts.fields = []gitReferenceField{gitReferenceFieldRefName, gitReferenceFieldObjectName}
}
if opts.maxWalkDistance <= 0 {
opts.maxWalkDistance = math.MaxInt32
}
if opts.patterns == nil {
opts.patterns = []string{}
}
if string(opts.sort) == "" {
opts.sort = gitReferenceFieldRefName
}
// prepare for-each-ref input
sortArg := mapToGiteaReferenceSortingArgument(opts.sort, opts.order)
rawFields := make([]string, len(opts.fields))
for i := range opts.fields {
rawFields[i] = string(opts.fields[i])
}
giteaFormat := gitearef.NewFormat(rawFields...)
// initializer pipeline for output processing
pipeOut, pipeIn := io.Pipe()
defer pipeOut.Close()
defer pipeIn.Close()
stderr := strings.Builder{}
rc := &gitea.RunOpts{Dir: repoPath, Stdout: pipeIn, Stderr: &stderr}
// create sort argument
go func() {
// create array for args as patterns have to be passed as separate args.
args := []string{
"for-each-ref",
"--format",
giteaFormat.Flag(),
"--sort",
sortArg,
"--count",
fmt.Sprint(opts.maxWalkDistance),
"--ignore-case",
}
args = append(args, opts.patterns...)
err := gitea.NewCommand(ctx, args...).Run(rc)
if err != nil {
_ = pipeIn.CloseWithError(gitea.ConcatenateError(err, stderr.String()))
} else {
_ = pipeIn.Close()
}
}()
parser := giteaFormat.Parser(pipeOut)
return walkGiteaReferenceParser(parser, handler, opts)
}
func walkGiteaReferenceParser(parser *gitearef.Parser, handler walkReferencesHandler,
opts *walkReferencesOptions) error {
for i := int32(0); i < opts.maxWalkDistance; i++ {
// parse next line - nil if end of output reached or an error occurred.
rawRef := parser.Next()
if rawRef == nil {
break
}
// convert to correct map.
ref, err := mapGiteaRawRef(rawRef)
if err != nil {
return err
}
// check with the instructor on the next instruction.
instruction, err := opts.instructor(ref)
if err != nil {
return fmt.Errorf("error getting instruction: %w", err)
}
if instruction == walkInstructionSkip {
continue
}
if instruction == walkInstructionStop {
break
}
// otherwise handle the reference.
err = handler(ref)
if err != nil {
return fmt.Errorf("error handling reference: %w", err)
}
}
if err := parser.Err(); err != nil {
return fmt.Errorf("failed to parse output: %w", err)
}
return nil
}
func mapGiteaRawRef(raw map[string]string) (map[gitReferenceField]string, error) {
res := make(map[gitReferenceField]string, len(raw))
for k, v := range raw {
gitRefField, err := parseGitReferenceField(k)
if err != nil {
return nil, err
}
res[gitRefField] = v
}
return res, nil
}
func mapToGiteaReferenceSortingArgument(s gitReferenceField, o sortOrder) string {
sortBy := string(gitReferenceFieldRefName)
desc := o == sortOrderDesc
if s == gitReferenceFieldCreatorDate {
sortBy = string(gitReferenceFieldCreatorDate)
if o == sortOrderDefault {
desc = true
}
}
if desc {
return "-" + sortBy
}
return sortBy
}
// GetSubmodule returns the submodule at the given path reachable from ref.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) GetSubmodule(ctx context.Context, repoPath string,
ref string, treePath string) (*submodule, error) {
treePath = cleanTreePath(treePath)
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
// Get the giteaCommit object for the ref
giteaCommit, err := giteaRepo.GetCommit(ref)
if err != nil {
return nil, fmt.Errorf("error getting commit for ref '%s': %w", ref, err)
}
giteaSubmodule, err := giteaCommit.GetSubModule(treePath)
if err != nil {
return nil, fmt.Errorf("error getting submodule '%s' from commit: %w", ref, err)
}
return &submodule{
name: giteaSubmodule.Name,
url: giteaSubmodule.URL,
}, nil
}
// GetBlob returns the blob at the given path reachable from ref.
// Note: sha is the object sha.
func (g giteaAdapter) GetBlob(ctx context.Context, repoPath string, sha string, sizeLimit int64) (*blob, error) {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return nil, err
}
defer giteaRepo.Close()
giteaBlob, err := giteaRepo.GetBlob(sha)
if err != nil {
return nil, fmt.Errorf("error getting blob '%s': %w", sha, err)
}
reader, err := giteaBlob.DataAsync()
if err != nil {
return nil, fmt.Errorf("error opening data for blob '%s': %w", sha, err)
}
returnSize := giteaBlob.Size()
if sizeLimit > 0 && returnSize > sizeLimit {
returnSize = sizeLimit
}
// TODO: ensure it doesn't fail because buff has exact size of bytes required
buff := make([]byte, returnSize)
_, err = io.ReadAtLeast(reader, buff, int(returnSize))
if err != nil {
return nil, fmt.Errorf("error reading data from blob '%s': %w", sha, err)
}
return &blob{
size: giteaBlob.Size(),
content: buff,
}, nil
}
func mapGiteaCommit(giteaCommit *gitea.Commit) (*commit, error) {
if giteaCommit == nil {
return nil, fmt.Errorf("gitea commit is nil")
}
author, err := mapGiteaSignature(giteaCommit.Author)
if err != nil {
return nil, fmt.Errorf("failed to map gitea author: %w", err)
}
committer, err := mapGiteaSignature(giteaCommit.Committer)
if err != nil {
return nil, fmt.Errorf("failed to map gitea commiter: %w", err)
}
return &commit{
sha: giteaCommit.ID.String(),
title: giteaCommit.Summary(),
// remove potential tailing newlines from message
message: strings.TrimRight(giteaCommit.Message(), "\n"),
author: author,
committer: committer,
}, nil
}
func mapGiteaNodeToTreeNodeModeAndType(giteaMode gitea.EntryMode) (treeNodeType, treeNodeMode, error) {
switch giteaMode {
case gitea.EntryModeBlob:
return treeNodeTypeBlob, treeNodeModeFile, nil
case gitea.EntryModeSymlink:
return treeNodeTypeBlob, treeNodeModeSymlink, nil
case gitea.EntryModeExec:
return treeNodeTypeBlob, treeNodeModeExec, nil
case gitea.EntryModeCommit:
return treeNodeTypeCommit, treeNodeModeCommit, nil
case gitea.EntryModeTree:
return treeNodeTypeTree, treeNodeModeTree, nil
default:
return treeNodeTypeBlob, treeNodeModeFile,
fmt.Errorf("received unknown tree node mode from gitea: '%s'", giteaMode.String())
}
}
func mapGiteaSignature(giteaSignature *gitea.Signature) (signature, error) {
if giteaSignature == nil {
return signature{}, fmt.Errorf("gitea signature is nil")
}
return signature{
identity: identity{
name: giteaSignature.Name,
email: giteaSignature.Email,
},
when: giteaSignature.When,
}, nil
}