mirror of
https://github.com/harness/drone.git
synced 2025-05-04 10:10:14 +08:00
376 lines
11 KiB
Go
376 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 service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/gitrpc/internal/tempdir"
|
|
"github.com/harness/gitness/gitrpc/internal/types"
|
|
"github.com/harness/gitness/gitrpc/rpc"
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// SharedRepo is a type to wrap our upload repositories as a shallow clone.
|
|
type SharedRepo struct {
|
|
repoUID string
|
|
repo *git.Repository
|
|
remoteRepo *git.Repository
|
|
TempBaseDir string
|
|
basePath string
|
|
}
|
|
|
|
// NewSharedRepo creates a new temporary upload repository.
|
|
func NewSharedRepo(tempDir, repoUID string, remoteRepo *git.Repository) (*SharedRepo, error) {
|
|
basePath, err := tempdir.CreateTemporaryPath(tempDir, repoUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t := &SharedRepo{
|
|
repoUID: repoUID,
|
|
remoteRepo: remoteRepo,
|
|
basePath: basePath,
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Close the repository cleaning up all files.
|
|
func (r *SharedRepo) Close() {
|
|
defer r.repo.Close()
|
|
if err := tempdir.RemoveTemporaryPath(r.basePath); err != nil {
|
|
log.Err(err).Msgf("Failed to remove temporary path %s", r.basePath)
|
|
}
|
|
}
|
|
|
|
// Clone the base repository to our path and set branch as the HEAD.
|
|
func (r *SharedRepo) Clone(ctx context.Context, branch string) error {
|
|
if _, _, err := git.NewCommand(ctx, "clone", "-s", "--bare", "-b",
|
|
branch, r.remoteRepo.Path, r.basePath).RunStdString(nil); err != nil {
|
|
stderr := err.Error()
|
|
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
|
return git.ErrBranchNotExist{
|
|
Name: branch,
|
|
}
|
|
} else if matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
|
|
return fmt.Errorf("%s %w", r.repoUID, types.ErrNotFound)
|
|
} else {
|
|
return fmt.Errorf("Clone: %w %s", err, stderr)
|
|
}
|
|
}
|
|
gitRepo, err := git.OpenRepository(ctx, r.basePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.repo = gitRepo
|
|
return nil
|
|
}
|
|
|
|
// Init the repository.
|
|
func (r *SharedRepo) Init(ctx context.Context) error {
|
|
if err := git.InitRepository(ctx, r.basePath, false); err != nil {
|
|
return err
|
|
}
|
|
gitRepo, err := git.OpenRepository(ctx, r.basePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.repo = gitRepo
|
|
return nil
|
|
}
|
|
|
|
// SetDefaultIndex sets the git index to our HEAD.
|
|
func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error {
|
|
if _, _, err := git.NewCommand(ctx, "read-tree", "HEAD").RunStdString(&git.RunOpts{Dir: r.basePath}); err != nil {
|
|
return fmt.Errorf("SetDefaultIndex: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LsFiles checks if the given filename arguments are in the index.
|
|
func (r *SharedRepo) LsFiles(ctx context.Context, filenames ...string) ([]string, error) {
|
|
stdOut := new(bytes.Buffer)
|
|
stdErr := new(bytes.Buffer)
|
|
|
|
cmdArgs := []string{"ls-files", "-z", "--"}
|
|
for _, arg := range filenames {
|
|
if arg != "" {
|
|
cmdArgs = append(cmdArgs, arg)
|
|
}
|
|
}
|
|
|
|
if err := git.NewCommand(ctx, cmdArgs...).
|
|
Run(&git.RunOpts{
|
|
Dir: r.basePath,
|
|
Stdout: stdOut,
|
|
Stderr: stdErr,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("unable to run git ls-files for temporary repo of: "+
|
|
"%s Error: %w\nstdout: %s\nstderr: %s",
|
|
r.repoUID, err, stdOut.String(), stdErr.String())
|
|
}
|
|
|
|
filelist := make([]string, 0)
|
|
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
|
|
filelist = append(filelist, string(line))
|
|
}
|
|
|
|
return filelist, nil
|
|
}
|
|
|
|
// RemoveFilesFromIndex removes the given files from the index.
|
|
func (r *SharedRepo) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error {
|
|
stdOut := new(bytes.Buffer)
|
|
stdErr := new(bytes.Buffer)
|
|
stdIn := new(bytes.Buffer)
|
|
for _, file := range filenames {
|
|
if file != "" {
|
|
stdIn.WriteString("0 0000000000000000000000000000000000000000\t")
|
|
stdIn.WriteString(file)
|
|
stdIn.WriteByte('\000')
|
|
}
|
|
}
|
|
|
|
if err := git.NewCommand(ctx, "update-index", "--remove", "-z", "--index-info").
|
|
Run(&git.RunOpts{
|
|
Dir: r.basePath,
|
|
Stdin: stdIn,
|
|
Stdout: stdOut,
|
|
Stderr: stdErr,
|
|
}); err != nil {
|
|
return fmt.Errorf("unable to update-index for temporary repo: %s Error: %w\nstdout: %s\nstderr: %s",
|
|
r.repoUID, err, stdOut.String(), stdErr.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HashObject writes the provided content to the object db and returns its hash.
|
|
func (r *SharedRepo) HashObject(ctx context.Context, content io.Reader) (string, error) {
|
|
stdOut := new(bytes.Buffer)
|
|
stdErr := new(bytes.Buffer)
|
|
|
|
if err := git.NewCommand(ctx, "hash-object", "-w", "--stdin").
|
|
Run(&git.RunOpts{
|
|
Dir: r.basePath,
|
|
Stdin: content,
|
|
Stdout: stdOut,
|
|
Stderr: stdErr,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s\nstderr: %s",
|
|
r.repoUID, err, stdOut.String(), stdErr.String())
|
|
}
|
|
|
|
return strings.TrimSpace(stdOut.String()), nil
|
|
}
|
|
|
|
// ShowFile dumps show file and write to io.Writer.
|
|
func (r *SharedRepo) ShowFile(ctx context.Context, filePath, commitHash string, writer io.Writer) error {
|
|
stderr := new(bytes.Buffer)
|
|
file := strings.TrimSpace(commitHash) + ":" + strings.TrimSpace(filePath)
|
|
cmd := git.NewCommand(ctx, "show", file)
|
|
if err := cmd.Run(&git.RunOpts{
|
|
Dir: r.repo.Path,
|
|
Stdout: writer,
|
|
Stderr: stderr,
|
|
}); err != nil {
|
|
return fmt.Errorf("show file: %w - %s", err, stderr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddObjectToIndex adds the provided object hash to the index with the provided mode and path.
|
|
func (r *SharedRepo) AddObjectToIndex(ctx context.Context, mode, objectHash, objectPath string) error {
|
|
if _, _, err := git.NewCommand(ctx, "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash,
|
|
objectPath).RunStdString(&git.RunOpts{Dir: r.basePath}); err != nil {
|
|
if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched {
|
|
return types.ErrInvalidPath
|
|
}
|
|
return fmt.Errorf("unable to add object to index at %s in temporary repo %s Error: %w",
|
|
objectPath, r.repoUID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteTree writes the current index as a tree to the object db and returns its hash.
|
|
func (r *SharedRepo) WriteTree(ctx context.Context) (string, error) {
|
|
stdout, _, err := git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: r.basePath})
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to write-tree in temporary repo for: %s Error: %w",
|
|
r.repoUID, err)
|
|
}
|
|
return strings.TrimSpace(stdout), nil
|
|
}
|
|
|
|
// GetLastCommit gets the last commit ID SHA of the repo.
|
|
func (r *SharedRepo) GetLastCommit(ctx context.Context) (string, error) {
|
|
return r.GetLastCommitByRef(ctx, "HEAD")
|
|
}
|
|
|
|
// GetLastCommitByRef gets the last commit ID SHA of the repo by ref.
|
|
func (r *SharedRepo) GetLastCommitByRef(ctx context.Context, ref string) (string, error) {
|
|
if ref == "" {
|
|
ref = "HEAD"
|
|
}
|
|
stdout, _, err := git.NewCommand(ctx, "rev-parse", ref).RunStdString(&git.RunOpts{Dir: r.basePath})
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to rev-parse %s in temporary repo for: %s Error: %w",
|
|
ref, r.repoUID, err)
|
|
}
|
|
return strings.TrimSpace(stdout), nil
|
|
}
|
|
|
|
// CommitTree creates a commit from a given tree for the user with provided message.
|
|
func (r *SharedRepo) CommitTree(ctx context.Context, parent string, author, committer *rpc.Identity, treeHash,
|
|
message string, signoff bool) (string, error) {
|
|
return r.CommitTreeWithDate(ctx, parent, author, committer, treeHash, message, signoff, time.Now(), time.Now())
|
|
}
|
|
|
|
// CommitTreeWithDate creates a commit from a given tree for the user with provided message.
|
|
func (r *SharedRepo) CommitTreeWithDate(
|
|
ctx context.Context,
|
|
parent string,
|
|
author, committer *rpc.Identity,
|
|
treeHash, message string,
|
|
signoff bool,
|
|
authorDate, committerDate time.Time,
|
|
) (string, error) {
|
|
committerSig := &git.Signature{
|
|
Name: committer.Name,
|
|
Email: committer.Email,
|
|
When: time.Now(),
|
|
}
|
|
|
|
// Because this may call hooks we should pass in the environment
|
|
env := append(os.Environ(),
|
|
"GIT_AUTHOR_NAME="+author.Name,
|
|
"GIT_AUTHOR_EMAIL="+author.Email,
|
|
"GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339),
|
|
"GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339),
|
|
)
|
|
|
|
messageBytes := new(bytes.Buffer)
|
|
_, _ = messageBytes.WriteString(message)
|
|
_, _ = messageBytes.WriteString("\n")
|
|
|
|
var args []string
|
|
if parent != "" {
|
|
args = []string{"commit-tree", treeHash, "-p", parent}
|
|
} else {
|
|
args = []string{"commit-tree", treeHash}
|
|
}
|
|
|
|
// temporary no signing
|
|
args = append(args, "--no-gpg-sign")
|
|
|
|
if signoff {
|
|
// Signed-off-by
|
|
_, _ = messageBytes.WriteString("\n")
|
|
_, _ = messageBytes.WriteString("Signed-off-by: ")
|
|
_, _ = messageBytes.WriteString(committerSig.String())
|
|
}
|
|
|
|
env = append(env,
|
|
"GIT_COMMITTER_NAME="+committerSig.Name,
|
|
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
|
)
|
|
|
|
stdout := new(bytes.Buffer)
|
|
stderr := new(bytes.Buffer)
|
|
if err := git.NewCommand(ctx, args...).
|
|
Run(&git.RunOpts{
|
|
Env: env,
|
|
Dir: r.basePath,
|
|
Stdin: messageBytes,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
}); err != nil {
|
|
return "", fmt.Errorf("unable to commit-tree in temporary repo: %s Error: %w\nStdout: %s\nStderr: %s",
|
|
r.repoUID, err, stdout, stderr)
|
|
}
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// Push the provided commitHash to the repository branch by the provided user.
|
|
func (r *SharedRepo) Push(ctx context.Context, doer *rpc.Identity, commitHash, branch string) error {
|
|
// Because calls hooks we need to pass in the environment
|
|
author, committer := doer, doer
|
|
env := PushingEnvironment(author, committer, r.repoUID)
|
|
if err := git.Push(ctx, r.basePath, git.PushOptions{
|
|
Remote: r.remoteRepo.Path,
|
|
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
|
|
Env: env,
|
|
}); err != nil {
|
|
if git.IsErrPushOutOfDate(err) {
|
|
return err
|
|
} else if git.IsErrPushRejected(err) {
|
|
rejectErr := new(git.ErrPushRejected)
|
|
if errors.As(err, &rejectErr) {
|
|
log.Info().Msgf("Unable to push back to repo from temporary repo due to rejection:"+
|
|
" %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
|
|
r.repoUID, r.basePath, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
|
|
}
|
|
return err
|
|
}
|
|
return fmt.Errorf("unable to push back to repo from temporary repo: %s (%s) Error: %w",
|
|
r.repoUID, r.basePath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetBranchCommit Gets the commit object of the given branch.
|
|
func (r *SharedRepo) GetBranchCommit(branch string) (*git.Commit, error) {
|
|
if r.repo == nil {
|
|
return nil, fmt.Errorf("repository has not been cloned")
|
|
}
|
|
return r.repo.GetBranchCommit(branch)
|
|
}
|
|
|
|
// GetCommit Gets the commit object of the given commit ID.
|
|
func (r *SharedRepo) GetCommit(commitID string) (*git.Commit, error) {
|
|
if r.repo == nil {
|
|
return nil, fmt.Errorf("repository has not been cloned")
|
|
}
|
|
return r.repo.GetCommit(commitID)
|
|
}
|
|
|
|
// PushingEnvironment returns an os environment to allow hooks to work on push.
|
|
func PushingEnvironment(
|
|
author,
|
|
committer *rpc.Identity,
|
|
repoUID string,
|
|
) []string {
|
|
authorSig := &git.Signature{
|
|
Name: author.Name,
|
|
Email: author.Email,
|
|
}
|
|
committerSig := &git.Signature{
|
|
Name: committer.Name,
|
|
Email: committer.Email,
|
|
}
|
|
|
|
environ := append(os.Environ(),
|
|
"GIT_AUTHOR_NAME="+authorSig.Name,
|
|
"GIT_AUTHOR_EMAIL="+authorSig.Email,
|
|
"GIT_COMMITTER_NAME="+committerSig.Name,
|
|
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
|
// important env vars for hooks
|
|
EnvPusherName+"="+committer.Name,
|
|
// EnvPusherID+"="+fmt.Sprintf("%d", committer.ID),
|
|
EnvRepoID+"="+repoUID,
|
|
EnvAppURL+"=", // app url
|
|
)
|
|
|
|
return environ
|
|
}
|