drone/internal/gitrpc/gitea.go
Johannes Batzill 91a75ed601 Add GetContent and ListCommits APIs, Fix DefaultBranch support (#32)
Adds the following:
- Add GetContent API (with gitrpc, proto, gitadapter changes)
- Add ListCommits API (with gitrpc, proto, gitadapter changes)
- DefaultBranch (to repo table in DB, update branch in git-repo, have default value in config)
2022-10-17 00:14:31 -07:00

414 lines
11 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"
"fmt"
"io"
"path"
"path/filepath"
"strings"
gitea "code.gitea.io/gitea/modules/git"
)
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) error {
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
if err != nil {
return err
}
defer giteaRepo.Close()
return giteaRepo.SetDefaultBranch(defaultBranch)
}
func (g giteaAdapter) Clone(ctx context.Context, from, to string, opts cloneRepoOption) 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 nodes of a tree reachable from ref via the specified path.
// Note: ref can be Branch / Tag / CommitSHA.
func (g giteaAdapter) ListTreeNodes(ctx context.Context, repoPath string,
ref string, treePath string, recursive bool) ([]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)
}
// 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)
}
nodes := make([]treeNode, 0, len(giteaEntries))
for _, giteaNode := range giteaEntries {
var nodeType treeNodeType
var mode treeNodeMode
nodeType, mode, err = mapGiteaNodeToTreeNodeModeAndType(giteaNode.Mode())
if err != nil {
return nil, err
}
// giteaNode.Name() returns the path of the node relative to the tree.
relPath := giteaNode.Name()
name := filepath.Base(relPath)
nodes = append(nodes, treeNode{
nodeType: nodeType,
mode: mode,
sha: giteaNode.ID.String(),
name: name,
path: filepath.Join(treePath, relPath),
})
}
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, 0, len(giteaCommits))
for _, giteaCommit := range giteaCommits {
var commit *commit
commit, err = mapGiteaCommit(giteaCommit)
if err != nil {
return nil, 0, err
}
commits = append(commits, *commit)
}
// TODO: save to cast to int from int64, or we expect exceeding int.MaxValue?
return commits, totalCount, nil
}
// 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) {
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(),
message: giteaCommit.Message(),
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 empty")
}
return signature{
identity: identity{
name: giteaSignature.Name,
email: giteaSignature.Email,
},
when: giteaSignature.When,
}, nil
}