mirror of https://github.com/harness/drone.git
528 lines
14 KiB
Go
528 lines
14 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package sharedrepo
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/harness/gitness/errors"
|
|
"github.com/harness/gitness/git/command"
|
|
"github.com/harness/gitness/git/tempdir"
|
|
"github.com/harness/gitness/git/types"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type SharedRepo struct {
|
|
temporaryPath string
|
|
repositoryPath string
|
|
}
|
|
|
|
// NewSharedRepo creates a new temporary bare repository.
|
|
func NewSharedRepo(
|
|
baseTmpDir string,
|
|
repositoryPath string,
|
|
) (*SharedRepo, error) {
|
|
var buf [5]byte
|
|
_, _ = rand.Read(buf[:])
|
|
id := base32.StdEncoding.EncodeToString(buf[:])
|
|
|
|
temporaryPath, err := tempdir.CreateTemporaryPath(baseTmpDir, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create shared repository directory: %w", err)
|
|
}
|
|
|
|
t := &SharedRepo{
|
|
temporaryPath: temporaryPath,
|
|
repositoryPath: repositoryPath,
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (r *SharedRepo) Close(ctx context.Context) {
|
|
if err := tempdir.RemoveTemporaryPath(r.temporaryPath); err != nil {
|
|
log.Ctx(ctx).Err(err).
|
|
Str("path", r.temporaryPath).
|
|
Msg("Failed to remove temporary shared directory")
|
|
}
|
|
}
|
|
|
|
func (r *SharedRepo) InitAsBare(ctx context.Context) error {
|
|
cmd := command.New("init", command.WithFlag("--bare"))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
|
return fmt.Errorf("failed to initialize bare git repository directory: %w", err)
|
|
}
|
|
|
|
if err := func() error {
|
|
alternates := filepath.Join(r.temporaryPath, "objects", "info", "alternates")
|
|
f, err := os.OpenFile(alternates, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create alternates file: %w", err)
|
|
}
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
data := filepath.Join(r.repositoryPath, "objects")
|
|
if _, err = fmt.Fprintln(f, data); err != nil {
|
|
return fmt.Errorf("failed to write alternates file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return fmt.Errorf("failed to make the alternates file in shared repository: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *SharedRepo) Directory() string {
|
|
return r.temporaryPath
|
|
}
|
|
|
|
// SetDefaultIndex sets the git index to our HEAD.
|
|
func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error {
|
|
cmd := command.New("read-tree", command.WithArg("HEAD"))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
|
return fmt.Errorf("failed to initialize shared repository index to HEAD: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetIndex sets the git index to the provided treeish.
|
|
func (r *SharedRepo) SetIndex(ctx context.Context, treeish string) error {
|
|
cmd := command.New("read-tree", command.WithArg(treeish))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
|
return fmt.Errorf("failed to initialize shared repository index to %q: %w", treeish, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ClearIndex clears the git index.
|
|
func (r *SharedRepo) ClearIndex(ctx context.Context) error {
|
|
cmd := command.New("read-tree", command.WithFlag("--empty"))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
|
return fmt.Errorf("failed to clear shared repository index: %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) {
|
|
cmd := command.New("ls-files", command.WithFlag("-z"), command.WithPostSepArg(filenames...))
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list files in shared repository's git index: %w", err)
|
|
}
|
|
|
|
files := make([]string, 0)
|
|
for _, line := range bytes.Split(stdout.Bytes(), []byte{'\000'}) {
|
|
files = append(files, string(line))
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// RemoveFilesFromIndex removes the given files from the index.
|
|
func (r *SharedRepo) RemoveFilesFromIndex(
|
|
ctx context.Context,
|
|
filenames ...string,
|
|
) error {
|
|
cmd := command.New("update-index",
|
|
command.WithFlag("--remove"),
|
|
command.WithFlag("-z"),
|
|
command.WithFlag("--index-info"))
|
|
|
|
stdin := bytes.NewBuffer(nil)
|
|
for _, file := range filenames {
|
|
if file != "" {
|
|
stdin.WriteString("0 0000000000000000000000000000000000000000\t")
|
|
stdin.WriteString(file)
|
|
stdin.WriteByte('\000')
|
|
}
|
|
}
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdin(stdin)); err != nil {
|
|
return fmt.Errorf("failed to update-index in shared repo: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteGitObject writes the provided content to the object db and returns its hash.
|
|
func (r *SharedRepo) WriteGitObject(
|
|
ctx context.Context,
|
|
content io.Reader,
|
|
) (string, error) {
|
|
cmd := command.New("hash-object",
|
|
command.WithFlag("-w"),
|
|
command.WithFlag("--stdin"))
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
err := cmd.Run(ctx,
|
|
command.WithDir(r.temporaryPath),
|
|
command.WithStdin(content),
|
|
command.WithStdout(stdout))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to hash-object in shared repo: %w", err)
|
|
}
|
|
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// ShowFile dumps show file and write to io.Writer.
|
|
func (r *SharedRepo) ShowFile(
|
|
ctx context.Context,
|
|
filePath string,
|
|
rev string,
|
|
writer io.Writer,
|
|
) error {
|
|
file := strings.TrimSpace(rev) + ":" + strings.TrimSpace(filePath)
|
|
|
|
cmd := command.New("show", command.WithArg(file))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(writer)); err != nil {
|
|
return fmt.Errorf("failed to show file in shared repo: %w", err)
|
|
}
|
|
|
|
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 string,
|
|
objectHash string,
|
|
objectPath string,
|
|
) error {
|
|
cmd := command.New("update-index",
|
|
command.WithFlag("--add"),
|
|
command.WithFlag("--replace"),
|
|
command.WithFlag("--cacheinfo", mode, objectHash, objectPath))
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
|
if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched {
|
|
return errors.InvalidArgument("invalid path '%s'", objectPath)
|
|
}
|
|
return fmt.Errorf("failed to add object to index in shared repo (path=%s): %w", objectPath, 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) {
|
|
cmd := command.New("write-tree")
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout)); err != nil {
|
|
return "", fmt.Errorf("failed to write-tree in shared repo: %w", err)
|
|
}
|
|
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// MergeTree merges commits in git index.
|
|
func (r *SharedRepo) MergeTree(
|
|
ctx context.Context,
|
|
commitMergeBase, commitTarget, commitSource string,
|
|
) (string, []string, error) {
|
|
cmd := command.New("merge-tree",
|
|
command.WithFlag("--write-tree"),
|
|
command.WithFlag("--name-only"),
|
|
command.WithFlag("--no-messages"),
|
|
command.WithArg(commitTarget),
|
|
command.WithArg(commitSource))
|
|
|
|
if commitMergeBase != "" {
|
|
cmd.Add(command.WithFlag("--merge-base=" + commitMergeBase))
|
|
}
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
err := cmd.Run(ctx,
|
|
command.WithDir(r.temporaryPath),
|
|
command.WithStdout(stdout))
|
|
|
|
// no error: the output is just the tree object SHA
|
|
if err == nil {
|
|
return strings.TrimSpace(stdout.String()), nil, nil
|
|
}
|
|
|
|
// exit code=1: the output is the tree object SHA, and list of files in conflict.
|
|
if cErr := command.AsError(err); cErr != nil && cErr.ExitCode() == 1 {
|
|
output := strings.TrimSpace(stdout.String())
|
|
lines := strings.Split(output, "\n")
|
|
if len(lines) < 2 {
|
|
log.Ctx(ctx).Err(err).Str("output", output).Msg("unexpected output of merge-tree in shared repo")
|
|
return "", nil, fmt.Errorf("unexpected output of merge-tree in shared repo: %w", err)
|
|
}
|
|
return lines[0], lines[1:], nil
|
|
}
|
|
|
|
return "", nil, fmt.Errorf("failed to merge-tree in shared repo: %w", err)
|
|
}
|
|
|
|
// CommitTree creates a commit from a given tree for the user with provided message.
|
|
func (r *SharedRepo) CommitTree(
|
|
ctx context.Context,
|
|
author, committer *types.Signature,
|
|
treeHash, message string,
|
|
signoff bool,
|
|
parentCommits ...string,
|
|
) (string, error) {
|
|
cmd := command.New("commit-tree",
|
|
command.WithArg(treeHash),
|
|
command.WithAuthorAndDate(
|
|
author.Identity.Name,
|
|
author.Identity.Email,
|
|
author.When,
|
|
),
|
|
command.WithCommitterAndDate(
|
|
committer.Identity.Name,
|
|
committer.Identity.Email,
|
|
committer.When,
|
|
),
|
|
)
|
|
|
|
for _, parentCommit := range parentCommits {
|
|
cmd.Add(command.WithFlag("-p", parentCommit))
|
|
}
|
|
|
|
// temporary no signing
|
|
cmd.Add(command.WithFlag("--no-gpg-sign"))
|
|
|
|
messageBytes := new(bytes.Buffer)
|
|
_, _ = messageBytes.WriteString(message)
|
|
_, _ = messageBytes.WriteString("\n")
|
|
|
|
if signoff {
|
|
// Signed-off-by
|
|
_, _ = messageBytes.WriteString("\n")
|
|
_, _ = messageBytes.WriteString("Signed-off-by: ")
|
|
_, _ = messageBytes.WriteString(fmt.Sprintf("%s <%s>", committer.Identity.Name, committer.Identity.Email))
|
|
}
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
err := cmd.Run(ctx,
|
|
command.WithDir(r.temporaryPath),
|
|
command.WithStdout(stdout),
|
|
command.WithStdin(messageBytes))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to commit-tree in shared repo: %w", err)
|
|
}
|
|
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// CommitSHAList returns list of SHAs of the commits between the two git revisions.
|
|
func (r *SharedRepo) CommitSHAList(
|
|
ctx context.Context,
|
|
start, end string,
|
|
) ([]string, error) {
|
|
cmd := command.New("rev-list", command.WithArg(start+".."+end))
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout)); err != nil {
|
|
return nil, fmt.Errorf("failed to rev-list in shared repo: %w", err)
|
|
}
|
|
|
|
var commitSHAs []string
|
|
|
|
scan := bufio.NewScanner(stdout)
|
|
for scan.Scan() {
|
|
commitSHA := scan.Text()
|
|
commitSHAs = append(commitSHAs, commitSHA)
|
|
}
|
|
if err := scan.Err(); err != nil {
|
|
return nil, fmt.Errorf("failed to scan rev-list output in shared repo: %w", err)
|
|
}
|
|
|
|
return commitSHAs, nil
|
|
}
|
|
|
|
// MergeBase returns number of commits between the two git revisions.
|
|
func (r *SharedRepo) MergeBase(
|
|
ctx context.Context,
|
|
rev1, rev2 string,
|
|
) (string, error) {
|
|
cmd := command.New("merge-base", command.WithArg(rev1), command.WithArg(rev2))
|
|
|
|
stdout := bytes.NewBuffer(nil)
|
|
|
|
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout)); err != nil {
|
|
return "", fmt.Errorf("failed to merge-base in shared repo: %w", err)
|
|
}
|
|
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// MoveObjects moves git object from the shared repository to the original repository.
|
|
func (r *SharedRepo) MoveObjects(ctx context.Context) error {
|
|
srcDir := path.Join(r.temporaryPath, "objects")
|
|
dstDir := path.Join(r.repositoryPath, "objects")
|
|
|
|
var files []fileEntry
|
|
|
|
err := filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
relPath, err := filepath.Rel(srcDir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get relative path: %w", err)
|
|
}
|
|
|
|
// avoid coping anything in the info/
|
|
if strings.HasPrefix(relPath, "info/") {
|
|
return nil
|
|
}
|
|
|
|
fileName := filepath.Base(relPath)
|
|
|
|
files = append(files, fileEntry{
|
|
fileName: fileName,
|
|
fullPath: path,
|
|
relPath: relPath,
|
|
priority: filePriority(fileName),
|
|
})
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list files of shared repository directory: %w", err)
|
|
}
|
|
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].priority < files[j].priority // 0 is top priority, 5 is lowest priority
|
|
})
|
|
|
|
for _, f := range files {
|
|
dstPath := filepath.Join(dstDir, f.relPath)
|
|
|
|
err = os.MkdirAll(filepath.Dir(dstPath), os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create directory for git object: %w", err)
|
|
}
|
|
|
|
// Try to move the file
|
|
|
|
errRename := os.Rename(f.fullPath, dstPath)
|
|
if errRename == nil {
|
|
log.Ctx(ctx).Debug().
|
|
Str("object", f.relPath).
|
|
Msg("moved git object")
|
|
continue
|
|
}
|
|
|
|
// Try to copy the file
|
|
|
|
copyError := func() error {
|
|
srcFile, err := os.Open(f.fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open source file: %w", err)
|
|
}
|
|
defer func() { _ = srcFile.Close() }()
|
|
|
|
dstFile, err := os.Create(dstPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create target file: %w", err)
|
|
}
|
|
defer func() { _ = dstFile.Close() }()
|
|
|
|
_, err = io.Copy(dstFile, srcFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy file content: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
if copyError != nil {
|
|
log.Ctx(ctx).Err(copyError).
|
|
Str("object", f.relPath).
|
|
Str("renameErr", errRename.Error()).
|
|
Msg("failed to move or copy git object")
|
|
return fmt.Errorf("failed to move or copy git object: %w", copyError)
|
|
}
|
|
|
|
log.Ctx(ctx).Warn().
|
|
Str("object", f.relPath).
|
|
Str("renameErr", errRename.Error()).
|
|
Msg("copied git object")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// filePriority is based on https://github.com/git/git/blob/master/tmp-objdir.c#L168
|
|
func filePriority(name string) int {
|
|
switch {
|
|
case !strings.HasPrefix(name, "pack"):
|
|
return 0
|
|
case strings.HasSuffix(name, ".keep"):
|
|
return 1
|
|
case strings.HasSuffix(name, ".pack"):
|
|
return 2
|
|
case strings.HasSuffix(name, ".rev"):
|
|
return 3
|
|
case strings.HasSuffix(name, ".idx"):
|
|
return 4
|
|
default:
|
|
return 5
|
|
}
|
|
}
|
|
|
|
type fileEntry struct {
|
|
fileName string
|
|
fullPath string
|
|
relPath string
|
|
priority int
|
|
}
|