mirror of
https://github.com/harness/drone.git
synced 2025-05-04 18:00:16 +08:00

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)
414 lines
11 KiB
Go
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
|
|
}
|