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

This change adds the following: - Inject APP server zerolog RequestID as metadata into all gitrpc calls from client side. - Inject Zerolog logger into context with common fields set (like service, method, requestID, ...). This allows for better request tracking within gitrpc, but also request tracking across services we extract the requestID send by the grpc client - Modify http logs to use http. prefix for common http related logging annotations.
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(ctx context.Context) {
|
|
defer r.repo.Close()
|
|
if err := tempdir.RemoveTemporaryPath(r.basePath); err != nil {
|
|
log.Ctx(ctx).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.Ctx(ctx).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
|
|
}
|