drone/internal/gitrpc/gitea.go
Johannes Batzill e423ffda3c Fix Default Branch Creation (#37)
While the HEAD is pointed to the correct branch (might not exist), we created the initial files during repo creation still on the master branch (as it's an empty repo and clone by default sets up master when cloning an empty repo)
2022-10-17 19:09:46 -07:00

429 lines
12 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, 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 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, 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)
nodes[i] = treeNode{
nodeType: nodeType,
mode: mode,
sha: giteaEntry.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, 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
}
// 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
}