mirror of https://github.com/harness/drone.git
refactor git hook calling and shared repo (#1200)
parent
740d47529a
commit
781b3547c0
|
@ -37,7 +37,7 @@ type ControllerClientFactory struct {
|
|||
git git.Interface
|
||||
}
|
||||
|
||||
func (f *ControllerClientFactory) NewClient(_ context.Context, envVars map[string]string) (hook.Client, error) {
|
||||
func (f *ControllerClientFactory) NewClient(envVars map[string]string) (hook.Client, error) {
|
||||
payload, err := hook.LoadPayloadFromMap[githook.Payload](envVars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load payload from provided map of environment variables: %w", err)
|
||||
|
|
|
@ -61,30 +61,26 @@ func (c *Controller) PreReceive(
|
|||
return output, nil
|
||||
}
|
||||
|
||||
if in.Internal {
|
||||
// It's an internal call, so no need to verify protection rules.
|
||||
return output, nil
|
||||
}
|
||||
|
||||
if c.blockPullReqRefUpdate(refUpdates) {
|
||||
// For external calls (git pushes) block modification of pullreq references.
|
||||
if !in.Internal && c.blockPullReqRefUpdate(refUpdates) {
|
||||
output.Error = ptr.String(usererror.ErrPullReqRefsCantBeModified.Error())
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// TODO: use store.PrincipalInfoCache once we abstracted principals.
|
||||
principal, err := c.principalStore.Find(ctx, in.PrincipalID)
|
||||
if err != nil {
|
||||
return hook.Output{}, fmt.Errorf("failed to find inner principal with id %d: %w", in.PrincipalID, err)
|
||||
}
|
||||
// For internal calls - through the application interface (API) - no need to verify protection rules.
|
||||
if !in.Internal {
|
||||
// TODO: use store.PrincipalInfoCache once we abstracted principals.
|
||||
principal, err := c.principalStore.Find(ctx, in.PrincipalID)
|
||||
if err != nil {
|
||||
return hook.Output{}, fmt.Errorf("failed to find inner principal with id %d: %w", in.PrincipalID, err)
|
||||
}
|
||||
|
||||
dummySession := &auth.Session{
|
||||
Principal: *principal,
|
||||
Metadata: nil,
|
||||
}
|
||||
dummySession := &auth.Session{Principal: *principal, Metadata: nil}
|
||||
|
||||
err = c.checkProtectionRules(ctx, dummySession, repo, refUpdates, &output)
|
||||
if err != nil {
|
||||
return hook.Output{}, fmt.Errorf("failed to check protection rules: %w", err)
|
||||
err = c.checkProtectionRules(ctx, dummySession, repo, refUpdates, &output)
|
||||
if err != nil {
|
||||
return hook.Output{}, fmt.Errorf("failed to check protection rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = c.scanSecrets(ctx, rgit, repo, in, &output)
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/harness/gitness/app/services/protection"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
@ -41,7 +42,7 @@ type CommitFileAction struct {
|
|||
// The provided value is compared against the latest sha of the file that's being updated.
|
||||
// If the SHA doesn't match, the update fails.
|
||||
// WARNING: If no SHA is provided, the update action will blindly overwrite the file's content.
|
||||
SHA string `json:"sha"`
|
||||
SHA sha.SHA `json:"sha"`
|
||||
}
|
||||
|
||||
// CommitFilesOptions holds the data for file operations.
|
||||
|
|
|
@ -44,7 +44,7 @@ const (
|
|||
// RestClientFactory creates clients that make rest api calls to gitness to execute githooks.
|
||||
type RestClientFactory struct{}
|
||||
|
||||
func (f *RestClientFactory) NewClient(_ context.Context, envVars map[string]string) (hook.Client, error) {
|
||||
func (f *RestClientFactory) NewClient(envVars map[string]string) (hook.Client, error) {
|
||||
payload, err := hook.LoadPayloadFromMap[Payload](envVars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load payload from provided map of environment variables: %w", err)
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/harness/gitness/git"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/store/database/dbtx"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
@ -62,7 +63,7 @@ func (r *Repository) processPipelines(ctx context.Context,
|
|||
Action: git.CreateAction,
|
||||
Path: file.ConvertedPath,
|
||||
Payload: file.Content,
|
||||
SHA: "",
|
||||
SHA: sha.None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
return nil, err
|
||||
}
|
||||
storageStore := storage.ProvideLocalStore()
|
||||
gitInterface, err := git.ProvideService(typesConfig, apiGit, storageStore)
|
||||
gitInterface, err := git.ProvideService(typesConfig, apiGit, clientFactory, storageStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
168
git/api/ref.go
168
git/api/ref.go
|
@ -26,10 +26,7 @@ import (
|
|||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api/foreachref"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// GitReferenceField represents the different fields available When listing references.
|
||||
|
@ -249,152 +246,29 @@ func (g *Git) GetRef(
|
|||
return sha.New(output.String())
|
||||
}
|
||||
|
||||
// UpdateRef allows to update / create / delete references
|
||||
// IMPORTANT provide full reference name to limit risk of collisions across reference types
|
||||
// (e.g `refs/heads/main` instead of `main`).
|
||||
func (g *Git) UpdateRef(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
oldValue sha.SHA,
|
||||
newValue sha.SHA,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
// GetReferenceFromBranchName assumes the provided value is the branch name (not the ref!)
|
||||
// and first sanitizes the branch name (remove any spaces or 'refs/heads/' prefix)
|
||||
// It then returns the full form of the branch reference.
|
||||
func GetReferenceFromBranchName(branchName string) string {
|
||||
// remove spaces
|
||||
branchName = strings.TrimSpace(branchName)
|
||||
// remove `refs/heads/` prefix (shouldn't be there, but if it is remove it to try to avoid complications)
|
||||
// NOTE: This is used to reduce missconfigurations via api
|
||||
// TODO: block via CLI, too
|
||||
branchName = strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)
|
||||
|
||||
// don't break existing interface - user calls with empty value to delete the ref.
|
||||
if newValue.IsEmpty() {
|
||||
newValue = sha.Nil
|
||||
}
|
||||
|
||||
// if no old value was provided, use current value (as required for hooks)
|
||||
// TODO: technically a delete could fail if someone updated the ref in the meanwhile.
|
||||
//nolint:gocritic,nestif
|
||||
if oldValue.IsEmpty() {
|
||||
val, err := g.GetRef(ctx, repoPath, ref)
|
||||
if errors.IsNotFound(err) {
|
||||
// fail in case someone tries to delete a reference that doesn't exist.
|
||||
if newValue.IsNil() {
|
||||
return errors.NotFound("reference %q not found", ref)
|
||||
}
|
||||
|
||||
oldValue = sha.Nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to get current value of reference: %w", err)
|
||||
} else {
|
||||
oldValue = val
|
||||
}
|
||||
}
|
||||
|
||||
err := g.updateRefWithHooks(
|
||||
ctx,
|
||||
envVars,
|
||||
repoPath,
|
||||
ref,
|
||||
oldValue,
|
||||
newValue,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update reference with hooks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// return reference
|
||||
return gitReferenceNamePrefixBranch + branchName
|
||||
}
|
||||
|
||||
// updateRefWithHooks performs a git-ref update for the provided reference.
|
||||
// Requires both old and new value to be provided explcitly, or the call fails (ensures consistency across operation).
|
||||
// pre-receice will be called before the update, post-receive after.
|
||||
func (g *Git) updateRefWithHooks(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
oldValue sha.SHA,
|
||||
newValue sha.SHA,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
func GetReferenceFromTagName(tagName string) string {
|
||||
// remove spaces
|
||||
tagName = strings.TrimSpace(tagName)
|
||||
// remove `refs/heads/` prefix (shouldn't be there, but if it is remove it to try to avoid complications)
|
||||
// NOTE: This is used to reduce missconfigurations via api
|
||||
// TODO: block via CLI, too
|
||||
tagName = strings.TrimPrefix(tagName, gitReferenceNamePrefixTag)
|
||||
|
||||
if oldValue.IsEmpty() {
|
||||
return fmt.Errorf("oldValue can't be empty")
|
||||
}
|
||||
if newValue.IsEmpty() {
|
||||
return fmt.Errorf("newValue can't be empty")
|
||||
}
|
||||
if oldValue.IsNil() && newValue.IsNil() {
|
||||
return fmt.Errorf("provided values cannot be both empty")
|
||||
}
|
||||
|
||||
githookClient, err := g.githookFactory.NewClient(ctx, envVars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create githook client: %w", err)
|
||||
}
|
||||
|
||||
// call pre-receive before updating the reference
|
||||
out, err := githookClient.PreReceive(ctx, hook.PreReceiveInput{
|
||||
RefUpdates: []hook.ReferenceUpdate{
|
||||
{
|
||||
Ref: ref,
|
||||
Old: oldValue,
|
||||
New: newValue,
|
||||
},
|
||||
},
|
||||
Environment: hook.Environment{
|
||||
// TODO: Update once we properly copy quarantine objects only after pre-receive.
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pre-receive call failed with: %w", err)
|
||||
}
|
||||
if out.Error != nil {
|
||||
return fmt.Errorf("pre-receive call returned error: %q", *out.Error)
|
||||
}
|
||||
|
||||
if g.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "pre-receive").
|
||||
Msgf("pre-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
|
||||
}
|
||||
|
||||
cmd := command.New("update-ref")
|
||||
if newValue.IsNil() {
|
||||
cmd.Add(command.WithFlag("-d", ref))
|
||||
} else {
|
||||
cmd.Add(command.WithArg(ref, newValue.String()))
|
||||
}
|
||||
|
||||
cmd.Add(command.WithArg(oldValue.String()))
|
||||
err = cmd.Run(ctx, command.WithDir(repoPath))
|
||||
if err != nil {
|
||||
return processGitErrorf(err, "update of ref %q from %q to %q failed", ref, oldValue, newValue)
|
||||
}
|
||||
|
||||
// call post-receive after updating the reference
|
||||
out, err = githookClient.PostReceive(ctx, hook.PostReceiveInput{
|
||||
RefUpdates: []hook.ReferenceUpdate{
|
||||
{
|
||||
Ref: ref,
|
||||
Old: oldValue,
|
||||
New: newValue,
|
||||
},
|
||||
},
|
||||
Environment: hook.Environment{},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("post-receive call failed with: %w", err)
|
||||
}
|
||||
if out.Error != nil {
|
||||
return fmt.Errorf("post-receive call returned error: %q", *out.Error)
|
||||
}
|
||||
|
||||
if g.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "post-receive").
|
||||
Msgf("post-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
// return reference
|
||||
return gitReferenceNamePrefixTag + tagName
|
||||
}
|
||||
|
|
|
@ -1,648 +0,0 @@
|
|||
// 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 api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/git/tempdir"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// SharedRepo is a type to wrap our upload repositories as a shallow clone.
|
||||
type SharedRepo struct {
|
||||
git *Git
|
||||
repoUID string
|
||||
remoteRepoPath string
|
||||
RepoPath string
|
||||
}
|
||||
|
||||
// NewSharedRepo creates a new temporary upload repository.
|
||||
func NewSharedRepo(
|
||||
adapter *Git,
|
||||
baseTmpDir string,
|
||||
repoUID string,
|
||||
remoteRepoPath string,
|
||||
) (*SharedRepo, error) {
|
||||
tmpPath, err := tempdir.CreateTemporaryPath(baseTmpDir, repoUID) // Need better solution
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &SharedRepo{
|
||||
git: adapter,
|
||||
repoUID: repoUID,
|
||||
remoteRepoPath: remoteRepoPath,
|
||||
RepoPath: tmpPath,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Close the repository cleaning up all files.
|
||||
func (r *SharedRepo) Close(ctx context.Context) {
|
||||
if err := tempdir.RemoveTemporaryPath(r.RepoPath); err != nil {
|
||||
log.Ctx(ctx).Err(err).Msgf("Failed to remove temporary path %s", r.RepoPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (r *SharedRepo) MoveObjects(ctx context.Context) error {
|
||||
srcDir := path.Join(r.RepoPath, "objects")
|
||||
dstDir := path.Join(r.remoteRepoPath, "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 walk source 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 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 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 copyError
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("object", f.relPath).
|
||||
Str("renameErr", errRename.Error()).
|
||||
Msg("copied git object")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (r *SharedRepo) initRepository(ctx context.Context, alternateObjDirs ...string) error {
|
||||
cmd := command.New("init", command.WithFlag("--bare"))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
|
||||
return errors.Internal(err, "error while creating empty repository")
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
alternates := filepath.Join(r.RepoPath, "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 open alternates file '%s': %w", alternates, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
data := strings.Join(
|
||||
append(
|
||||
alternateObjDirs,
|
||||
filepath.Join(r.remoteRepoPath, "objects"),
|
||||
),
|
||||
"\n",
|
||||
)
|
||||
|
||||
if _, err = fmt.Fprintln(f, data); err != nil {
|
||||
return fmt.Errorf("failed to write alternates file '%s': %w", alternates, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Internal(err, "failed to create alternate in empty repository: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) InitAsShared(ctx context.Context) error {
|
||||
return r.initRepository(ctx)
|
||||
}
|
||||
|
||||
// InitAsSharedWithAlternates initializes repository with provided alternate object directories.
|
||||
func (r *SharedRepo) InitAsSharedWithAlternates(ctx context.Context, alternateObjDirs ...string) error {
|
||||
return r.initRepository(ctx, alternateObjDirs...)
|
||||
}
|
||||
|
||||
// Clone the base repository to our path and set branch as the HEAD.
|
||||
func (r *SharedRepo) Clone(ctx context.Context, branchName string) error {
|
||||
cmd := command.New("clone",
|
||||
command.WithFlag("-s"),
|
||||
command.WithFlag("--bare"),
|
||||
)
|
||||
if branchName != "" {
|
||||
cmd.Add(command.WithFlag("-b", strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)))
|
||||
}
|
||||
cmd.Add(command.WithArg(r.remoteRepoPath, r.RepoPath))
|
||||
|
||||
if err := cmd.Run(ctx); err != nil {
|
||||
cmderr := command.AsError(err)
|
||||
if cmderr.StdErr == nil {
|
||||
return errors.Internal(err, "error while cloning repository")
|
||||
}
|
||||
stderr := string(cmderr.StdErr)
|
||||
matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr)
|
||||
if matched {
|
||||
return errors.NotFound("branch '%s' does not exist", branchName)
|
||||
}
|
||||
matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr)
|
||||
if matched {
|
||||
return errors.NotFound("repository '%s' does not exist", r.repoUID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init the repository.
|
||||
func (r *SharedRepo) Init(ctx context.Context) error {
|
||||
err := r.git.InitRepository(ctx, r.RepoPath, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize shared repo: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultIndex sets the git index to our HEAD.
|
||||
func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error {
|
||||
return r.SetIndex(ctx, "HEAD")
|
||||
}
|
||||
|
||||
// SetIndex sets the git index to the provided treeish.
|
||||
func (r *SharedRepo) SetIndex(ctx context.Context, rev string) error {
|
||||
cmd := command.New("read-tree", command.WithArg(rev))
|
||||
if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
|
||||
return fmt.Errorf("failed to git read-tree %s: %w", rev, 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"),
|
||||
)
|
||||
|
||||
for _, arg := range filenames {
|
||||
if arg != "" {
|
||||
cmd.Add(command.WithPostSepArg(arg))
|
||||
}
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
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 {
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdIn := new(bytes.Buffer)
|
||||
for _, file := range filenames {
|
||||
if file != "" {
|
||||
stdIn.WriteString("0 0000000000000000000000000000000000000000\t")
|
||||
stdIn.WriteString(file)
|
||||
stdIn.WriteByte('\000')
|
||||
}
|
||||
}
|
||||
|
||||
cmd := command.New("update-index",
|
||||
command.WithFlag("--remove"),
|
||||
command.WithFlag("-z"),
|
||||
command.WithFlag("--index-info"),
|
||||
)
|
||||
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdin(stdIn),
|
||||
command.WithStdout(stdOut),
|
||||
); err != nil {
|
||||
return fmt.Errorf("unable to update-index for temporary repo: %s Error: %w\nstdout: %s",
|
||||
r.repoUID, err, stdOut.String())
|
||||
}
|
||||
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,
|
||||
) (sha.SHA, error) {
|
||||
stdOut := new(bytes.Buffer)
|
||||
cmd := command.New("hash-object",
|
||||
command.WithFlag("-w"),
|
||||
command.WithFlag("--stdin"),
|
||||
)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdin(content),
|
||||
command.WithStdout(stdOut),
|
||||
); err != nil {
|
||||
return sha.None, fmt.Errorf("unable to hash-object to temporary repo: %s Error: %w\nstdout: %s",
|
||||
r.repoUID, err, stdOut.String())
|
||||
}
|
||||
|
||||
return sha.New(stdOut.String())
|
||||
}
|
||||
|
||||
// ShowFile dumps show file and write to io.Writer.
|
||||
func (r *SharedRepo) ShowFile(
|
||||
ctx context.Context,
|
||||
filePath string,
|
||||
commitHash string,
|
||||
writer io.Writer,
|
||||
) error {
|
||||
file := strings.TrimSpace(commitHash) + ":" + strings.TrimSpace(filePath)
|
||||
cmd := command.New("show",
|
||||
command.WithArg(file),
|
||||
)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(writer),
|
||||
); err != nil {
|
||||
return fmt.Errorf("show file: %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"),
|
||||
command.WithArg(mode, objectHash, objectPath),
|
||||
)
|
||||
if err := cmd.Run(ctx, command.WithDir(r.RepoPath)); err != nil {
|
||||
if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched {
|
||||
return errors.InvalidArgument("invalid path '%s'", objectPath)
|
||||
}
|
||||
return fmt.Errorf("unable to add object to index at %s in temporary repo path %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) (sha.SHA, error) {
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd := command.New("write-tree")
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(stdout),
|
||||
)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("unable to write-tree in temporary repo path for: %s Error: %w",
|
||||
r.repoUID, err)
|
||||
}
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
// 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 := &bytes.Buffer{}
|
||||
cmd := command.New("rev-parse",
|
||||
command.WithArg(ref),
|
||||
)
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdout(stdout),
|
||||
)
|
||||
if err != nil {
|
||||
return "", processGitErrorf(err, "unable to rev-parse %s in temporary repo for: %s",
|
||||
ref, r.repoUID)
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// CommitTreeWithDate creates a commit from a given tree for the user with provided message.
|
||||
func (r *SharedRepo) CommitTreeWithDate(
|
||||
ctx context.Context,
|
||||
parent sha.SHA,
|
||||
author, committer *Identity,
|
||||
treeHash sha.SHA,
|
||||
message string,
|
||||
signoff bool,
|
||||
authorDate, committerDate time.Time,
|
||||
) (sha.SHA, error) {
|
||||
messageBytes := new(bytes.Buffer)
|
||||
_, _ = messageBytes.WriteString(message)
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
|
||||
cmd := command.New("commit-tree",
|
||||
command.WithAuthorAndDate(
|
||||
author.Name,
|
||||
author.Email,
|
||||
authorDate,
|
||||
),
|
||||
command.WithCommitterAndDate(
|
||||
committer.Name,
|
||||
committer.Email,
|
||||
committerDate,
|
||||
),
|
||||
)
|
||||
if !parent.IsEmpty() {
|
||||
cmd.Add(command.WithFlag("-p", parent.String()))
|
||||
}
|
||||
cmd.Add(command.WithArg(treeHash.String()))
|
||||
|
||||
// temporary no signing
|
||||
cmd.Add(command.WithFlag("--no-gpg-sign"))
|
||||
|
||||
if signoff {
|
||||
sig := &Signature{
|
||||
Identity: Identity{
|
||||
Name: committer.Name,
|
||||
Email: committer.Email,
|
||||
},
|
||||
When: committerDate,
|
||||
}
|
||||
// Signed-off-by
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
_, _ = messageBytes.WriteString("Signed-off-by: ")
|
||||
_, _ = messageBytes.WriteString(sig.String())
|
||||
}
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
if err := cmd.Run(ctx,
|
||||
command.WithDir(r.RepoPath),
|
||||
command.WithStdin(messageBytes),
|
||||
command.WithStdout(stdout),
|
||||
); err != nil {
|
||||
return sha.None, processGitErrorf(err, "unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s",
|
||||
r.repoUID, err, stdout)
|
||||
}
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
func (r *SharedRepo) PushDeleteBranch(
|
||||
ctx context.Context,
|
||||
branch string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
return r.push(ctx, "", GetReferenceFromBranchName(branch), force, env...)
|
||||
}
|
||||
|
||||
func (r *SharedRepo) PushCommitToBranch(
|
||||
ctx context.Context,
|
||||
commitSHA string,
|
||||
branch string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
return r.push(ctx,
|
||||
commitSHA,
|
||||
GetReferenceFromBranchName(branch),
|
||||
force,
|
||||
env...,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *SharedRepo) PushBranch(
|
||||
ctx context.Context,
|
||||
sourceBranch string,
|
||||
branch string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
return r.push(ctx,
|
||||
GetReferenceFromBranchName(sourceBranch),
|
||||
GetReferenceFromBranchName(branch),
|
||||
force,
|
||||
env...,
|
||||
)
|
||||
}
|
||||
func (r *SharedRepo) PushTag(
|
||||
ctx context.Context,
|
||||
tagName string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
refTag := GetReferenceFromTagName(tagName)
|
||||
return r.push(ctx, refTag, refTag, force, env...)
|
||||
}
|
||||
|
||||
func (r *SharedRepo) PushDeleteTag(
|
||||
ctx context.Context,
|
||||
tagName string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
refTag := GetReferenceFromTagName(tagName)
|
||||
return r.push(ctx, "", refTag, force, env...)
|
||||
}
|
||||
|
||||
// push pushes the provided references to the provided branch in the original repository.
|
||||
func (r *SharedRepo) push(
|
||||
ctx context.Context,
|
||||
sourceRef string,
|
||||
destinationRef string,
|
||||
force bool,
|
||||
env ...string,
|
||||
) error {
|
||||
// Because calls hooks we need to pass in the environment
|
||||
if err := r.git.Push(ctx, r.RepoPath, PushOptions{
|
||||
Remote: r.remoteRepoPath,
|
||||
Branch: sourceRef + ":" + destinationRef,
|
||||
Env: env,
|
||||
Force: force,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("unable to push back to repo from temporary repo: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBranch gets the branch object of the given ref.
|
||||
func (r *SharedRepo) GetBranch(ctx context.Context, rev string) (*Branch, error) {
|
||||
return r.git.GetBranch(ctx, r.RepoPath, rev)
|
||||
}
|
||||
|
||||
// GetCommit Gets the commit object of the given commit ID.
|
||||
func (r *SharedRepo) GetCommit(ctx context.Context, commitID string) (*Commit, error) {
|
||||
return r.git.GetCommit(ctx, r.RepoPath, commitID)
|
||||
}
|
||||
|
||||
// GetTreeNode Gets the tree node object of the given commit ID and path.
|
||||
func (r *SharedRepo) GetTreeNode(ctx context.Context, commitID, treePath string) (*TreeNode, error) {
|
||||
return r.git.GetTreeNode(ctx, r.RepoPath, commitID, treePath)
|
||||
}
|
||||
|
||||
// GetReferenceFromBranchName assumes the provided value is the branch name (not the ref!)
|
||||
// and first sanitizes the branch name (remove any spaces or 'refs/heads/' prefix)
|
||||
// It then returns the full form of the branch reference.
|
||||
func GetReferenceFromBranchName(branchName string) string {
|
||||
// remove spaces
|
||||
branchName = strings.TrimSpace(branchName)
|
||||
// remove `refs/heads/` prefix (shouldn't be there, but if it is remove it to try to avoid complications)
|
||||
// NOTE: This is used to reduce missconfigurations via api
|
||||
// TODO: block via CLI, too
|
||||
branchName = strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)
|
||||
|
||||
// return reference
|
||||
return gitReferenceNamePrefixBranch + branchName
|
||||
}
|
||||
|
||||
func GetReferenceFromTagName(tagName string) string {
|
||||
// remove spaces
|
||||
tagName = strings.TrimSpace(tagName)
|
||||
// remove `refs/heads/` prefix (shouldn't be there, but if it is remove it to try to avoid complications)
|
||||
// NOTE: This is used to reduce missconfigurations via api
|
||||
// TODO: block via CLI, too
|
||||
tagName = strings.TrimPrefix(tagName, gitReferenceNamePrefixTag)
|
||||
|
||||
// return reference
|
||||
return gitReferenceNamePrefixTag + tagName
|
||||
}
|
|
@ -237,6 +237,11 @@ func lsFile(
|
|||
|
||||
// GetTreeNode returns the tree node at the given path as found for the provided reference.
|
||||
func (g *Git) GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*TreeNode, error) {
|
||||
return GetTreeNode(ctx, repoPath, rev, treePath)
|
||||
}
|
||||
|
||||
// GetTreeNode returns the tree node at the given path as found for the provided reference.
|
||||
func GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*TreeNode, error) {
|
||||
if repoPath == "" {
|
||||
return nil, ErrRepositoryPathEmpty
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/check"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -101,20 +102,20 @@ func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams)
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get target commit: %w", err)
|
||||
}
|
||||
|
||||
branchRef := api.GetReferenceFromBranchName(params.BranchName)
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
branchRef,
|
||||
sha.Nil, // we want to make sure we don't overwrite any parallel create
|
||||
targetCommit.SHA,
|
||||
)
|
||||
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ref updater to create the branch: %w", err)
|
||||
}
|
||||
|
||||
err = refUpdater.Do(ctx, sha.Nil, targetCommit.SHA)
|
||||
if errors.IsConflict(err) {
|
||||
return nil, errors.Conflict("branch %q already exists", params.BranchName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update branch reference: %w", err)
|
||||
return nil, fmt.Errorf("failed to create branch reference: %w", err)
|
||||
}
|
||||
|
||||
commit, err := mapCommit(targetCommit)
|
||||
|
@ -162,14 +163,12 @@ func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams)
|
|||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
branchRef := api.GetReferenceFromBranchName(params.BranchName)
|
||||
|
||||
err := s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
branchRef,
|
||||
sha.None, // delete whatever is there
|
||||
sha.Nil,
|
||||
)
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ref updater to create the branch: %w", err)
|
||||
}
|
||||
|
||||
err = refUpdater.Do(ctx, sha.None, sha.Nil) // delete whatever is there
|
||||
if errors.IsNotFound(err) {
|
||||
return errors.NotFound("branch %q does not exist", params.BranchName)
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type Client interface {
|
|||
|
||||
// ClientFactory is an abstraction of a factory that creates a new client based on the provided environment variables.
|
||||
type ClientFactory interface {
|
||||
NewClient(ctx context.Context, envVars map[string]string) (Client, error)
|
||||
NewClient(envVars map[string]string) (Client, error)
|
||||
}
|
||||
|
||||
// TODO: move to single representation once we have our custom Git CLI wrapper.
|
||||
|
|
|
@ -0,0 +1,285 @@
|
|||
// 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 hook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CreateRefUpdater creates new RefUpdater object using the provided git hook ClientFactory.
|
||||
func CreateRefUpdater(
|
||||
hookClientFactory ClientFactory,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
) (*RefUpdater, error) {
|
||||
if repoPath == "" {
|
||||
return nil, errors.Internal(nil, "repo path can't be empty")
|
||||
}
|
||||
|
||||
client, err := hookClientFactory.NewClient(envVars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create hook.Client: %w", err)
|
||||
}
|
||||
|
||||
return &RefUpdater{
|
||||
state: statePre,
|
||||
hookClient: client,
|
||||
envVars: envVars,
|
||||
repoPath: repoPath,
|
||||
ref: ref,
|
||||
oldValue: sha.None,
|
||||
newValue: sha.None,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefUpdater is an entity that is responsible for update of a reference.
|
||||
// It will call pre-receive hook prior to the update and post-receive hook after the update.
|
||||
// It has a state machine to guarantee that methods are called in the correct order (Init, Pre, Update, Post).
|
||||
type RefUpdater struct {
|
||||
state refUpdaterState
|
||||
hookClient Client
|
||||
envVars map[string]string
|
||||
repoPath string
|
||||
ref string
|
||||
oldValue sha.SHA
|
||||
newValue sha.SHA
|
||||
}
|
||||
|
||||
// refUpdaterState represents state of the ref updater internal state machine.
|
||||
type refUpdaterState byte
|
||||
|
||||
func (t refUpdaterState) String() string {
|
||||
switch t {
|
||||
case statePre:
|
||||
return "PRE"
|
||||
case stateUpdate:
|
||||
return "UPDATE"
|
||||
case statePost:
|
||||
return "POST"
|
||||
case stateDone:
|
||||
return "DONE"
|
||||
}
|
||||
return "INVALID"
|
||||
}
|
||||
|
||||
const (
|
||||
statePre refUpdaterState = iota
|
||||
stateUpdate
|
||||
statePost
|
||||
stateDone
|
||||
)
|
||||
|
||||
// Do runs full ref update by executing all methods in the correct order.
|
||||
func (u *RefUpdater) Do(ctx context.Context, oldValue, newValue sha.SHA) error {
|
||||
if err := u.Init(ctx, oldValue, newValue); err != nil {
|
||||
return fmt.Errorf("init failed: %w", err)
|
||||
}
|
||||
|
||||
if err := u.Pre(ctx); err != nil {
|
||||
return fmt.Errorf("pre failed: %w", err)
|
||||
}
|
||||
|
||||
if err := u.UpdateRef(ctx); err != nil {
|
||||
return fmt.Errorf("update failed: %w", err)
|
||||
}
|
||||
|
||||
if err := u.Post(ctx); err != nil {
|
||||
return fmt.Errorf("post failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RefUpdater) Init(ctx context.Context, oldValue, newValue sha.SHA) error {
|
||||
if err := u.InitOld(ctx, oldValue); err != nil {
|
||||
return fmt.Errorf("init old failed: %w", err)
|
||||
}
|
||||
if err := u.InitNew(ctx, newValue); err != nil {
|
||||
return fmt.Errorf("init new failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RefUpdater) InitNew(_ context.Context, newValue sha.SHA) error {
|
||||
if u.state != statePre {
|
||||
return fmt.Errorf("invalid operation order: init requires state=%s, current state=%s",
|
||||
statePre, u.state)
|
||||
}
|
||||
|
||||
if newValue.IsEmpty() {
|
||||
// don't break existing interface - user calls with empty value to delete the ref.
|
||||
newValue = sha.Nil
|
||||
}
|
||||
|
||||
u.newValue = newValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RefUpdater) InitOld(ctx context.Context, oldValue sha.SHA) error {
|
||||
if u.state != statePre {
|
||||
return fmt.Errorf("invalid operation order: init requires state=%s, current state=%s",
|
||||
statePre, u.state)
|
||||
}
|
||||
|
||||
if oldValue.IsEmpty() {
|
||||
// if no old value was provided, use current value (as required for hooks)
|
||||
val, err := u.getRef(ctx)
|
||||
if errors.IsNotFound(err) { //nolint:gocritic
|
||||
oldValue = sha.Nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to get current value of reference: %w", err)
|
||||
} else {
|
||||
oldValue = val
|
||||
}
|
||||
}
|
||||
|
||||
u.oldValue = oldValue
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre runs the pre-receive git hook.
|
||||
func (u *RefUpdater) Pre(ctx context.Context, alternateDirs ...string) error {
|
||||
if u.state != statePre {
|
||||
return fmt.Errorf("invalid operation order: pre-receive hook requires state=%s, current state=%s",
|
||||
statePre, u.state)
|
||||
}
|
||||
|
||||
// fail in case someone tries to delete a reference that doesn't exist.
|
||||
if u.oldValue.IsEmpty() && u.newValue.IsNil() {
|
||||
return errors.NotFound("reference %q not found", u.ref)
|
||||
}
|
||||
|
||||
if u.oldValue.IsNil() && u.newValue.IsNil() {
|
||||
return fmt.Errorf("provided values cannot be both empty")
|
||||
}
|
||||
|
||||
out, err := u.hookClient.PreReceive(ctx, PreReceiveInput{
|
||||
RefUpdates: []ReferenceUpdate{
|
||||
{
|
||||
Ref: u.ref,
|
||||
Old: u.oldValue,
|
||||
New: u.newValue,
|
||||
},
|
||||
},
|
||||
Environment: Environment{
|
||||
AlternateObjectDirs: alternateDirs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pre-receive call failed with: %w", err)
|
||||
}
|
||||
if out.Error != nil {
|
||||
log.Ctx(ctx).Debug().
|
||||
Str("err", *out.Error).
|
||||
Msgf("Pre-receive blocked ref update\nMessages\n%v", out.Messages)
|
||||
return errors.PreconditionFailed("pre-receive hook blocked reference update: %q", *out.Error)
|
||||
}
|
||||
|
||||
u.state = stateUpdate
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRef updates the git reference.
|
||||
func (u *RefUpdater) UpdateRef(ctx context.Context) error {
|
||||
if u.state != stateUpdate {
|
||||
return fmt.Errorf("invalid operation order: ref update requires state=%s, current state=%s",
|
||||
stateUpdate, u.state)
|
||||
}
|
||||
|
||||
cmd := command.New("update-ref")
|
||||
if u.newValue.IsNil() {
|
||||
cmd.Add(command.WithFlag("-d", u.ref))
|
||||
} else {
|
||||
cmd.Add(command.WithArg(u.ref, u.newValue.String()))
|
||||
}
|
||||
|
||||
cmd.Add(command.WithArg(u.oldValue.String()))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(u.repoPath)); err != nil {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "reference already exists") {
|
||||
return errors.Conflict("reference already exists")
|
||||
}
|
||||
|
||||
return fmt.Errorf("update of ref %q from %q to %q failed: %w", u.ref, u.oldValue, u.newValue, err)
|
||||
}
|
||||
|
||||
u.state = statePost
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Post runs the pre-receive git hook.
|
||||
func (u *RefUpdater) Post(ctx context.Context, alternateDirs ...string) error {
|
||||
if u.state != statePost {
|
||||
return fmt.Errorf("invalid operation order: post-receive hook requires state=%s, current state=%s",
|
||||
statePost, u.state)
|
||||
}
|
||||
|
||||
out, err := u.hookClient.PostReceive(ctx, PostReceiveInput{
|
||||
RefUpdates: []ReferenceUpdate{
|
||||
{
|
||||
Ref: u.ref,
|
||||
Old: u.oldValue,
|
||||
New: u.newValue,
|
||||
},
|
||||
},
|
||||
Environment: Environment{
|
||||
AlternateObjectDirs: alternateDirs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("post-receive call failed with: %w", err)
|
||||
}
|
||||
if out.Error != nil {
|
||||
return fmt.Errorf("post-receive call returned error: %q", *out.Error)
|
||||
}
|
||||
|
||||
u.state = stateDone
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RefUpdater) getRef(ctx context.Context) (sha.SHA, error) {
|
||||
cmd := command.New("show-ref",
|
||||
command.WithFlag("--verify"),
|
||||
command.WithFlag("-s"),
|
||||
command.WithArg(u.ref),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(u.repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
if command.AsError(err).IsExitCode(128) && strings.Contains(err.Error(), "not a valid ref") {
|
||||
return sha.None, errors.NotFound("reference %q not found", u.ref)
|
||||
}
|
||||
return sha.None, err
|
||||
}
|
||||
|
||||
return sha.New(output.String())
|
||||
}
|
30
git/merge.go
30
git/merge.go
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/merge"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
|
@ -275,8 +276,18 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
|||
|
||||
// merge
|
||||
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, refPath)
|
||||
if err != nil {
|
||||
return MergeOutput{}, errors.Internal(err, "failed to create ref updater object")
|
||||
}
|
||||
|
||||
if err := refUpdater.InitOld(ctx, refOldValue); err != nil {
|
||||
return MergeOutput{}, errors.Internal(err, "failed to set old reference value for ref updater")
|
||||
}
|
||||
|
||||
mergeCommitSHA, conflicts, err := mergeFunc(
|
||||
ctx,
|
||||
refUpdater,
|
||||
repoPath, s.tmpDir,
|
||||
&author, &committer,
|
||||
mergeMsg,
|
||||
|
@ -297,25 +308,6 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// git reference update
|
||||
|
||||
log.Trace().Msg("merge completed - updating git reference")
|
||||
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
refPath,
|
||||
refOldValue,
|
||||
mergeCommitSHA,
|
||||
)
|
||||
if err != nil {
|
||||
return MergeOutput{},
|
||||
errors.Internal(err, "failed to update branch %q after merging commits", params.HeadBranch)
|
||||
}
|
||||
|
||||
log.Trace().Msg("merge completed - git reference updated")
|
||||
|
||||
return MergeOutput{
|
||||
BaseSHA: baseCommitSHA,
|
||||
HeadSHA: headCommitSHA,
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/git/sharedrepo"
|
||||
|
||||
|
@ -28,6 +29,7 @@ import (
|
|||
// Func represents a merge method function. The concrete merge implementation functions must have this signature.
|
||||
type Func func(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
|
@ -37,12 +39,14 @@ type Func func(
|
|||
// Merge merges two the commits (targetSHA and sourceSHA) using the Merge method.
|
||||
func Merge(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
return mergeInternal(ctx,
|
||||
refUpdater,
|
||||
repoPath, tmpDir,
|
||||
author, committer,
|
||||
message,
|
||||
|
@ -53,12 +57,14 @@ func Merge(
|
|||
// Squash merges two the commits (targetSHA and sourceSHA) using the Squash method.
|
||||
func Squash(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
return mergeInternal(ctx,
|
||||
refUpdater,
|
||||
repoPath, tmpDir,
|
||||
author, committer,
|
||||
message,
|
||||
|
@ -69,13 +75,14 @@ func Squash(
|
|||
// mergeInternal is internal implementation of merge used for Merge and Squash methods.
|
||||
func mergeInternal(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
repoPath, tmpDir string,
|
||||
author, committer *api.Signature,
|
||||
message string,
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
squash bool,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
|
||||
err = sharedrepo.Run(ctx, refUpdater, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
|
||||
var err error
|
||||
|
||||
var treeSHA sha.SHA
|
||||
|
@ -100,6 +107,10 @@ func mergeInternal(
|
|||
return fmt.Errorf("commit tree failed: %w", err)
|
||||
}
|
||||
|
||||
if err := refUpdater.InitNew(ctx, mergeSHA); err != nil {
|
||||
return fmt.Errorf("refUpdater.InitNew failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -114,12 +125,13 @@ func mergeInternal(
|
|||
//nolint:gocognit // refactor if needed.
|
||||
func Rebase(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
repoPath, tmpDir string,
|
||||
_, committer *api.Signature, // commit author isn't used here - it's copied from every commit
|
||||
_ string, // commit message isn't used here
|
||||
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
|
||||
) (mergeSHA sha.SHA, conflicts []string, err error) {
|
||||
err = runInSharedRepo(ctx, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
|
||||
err = sharedrepo.Run(ctx, refUpdater, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
|
||||
sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find commit list in rebase merge: %w", err)
|
||||
|
@ -183,6 +195,10 @@ func Rebase(
|
|||
lastTreeSHA = treeSHA
|
||||
}
|
||||
|
||||
if err := refUpdater.InitNew(ctx, lastCommitSHA); err != nil {
|
||||
return fmt.Errorf("refUpdater.InitNew failed: %w", err)
|
||||
}
|
||||
|
||||
mergeSHA = lastCommitSHA
|
||||
|
||||
return nil
|
||||
|
@ -193,34 +209,3 @@ func Rebase(
|
|||
|
||||
return mergeSHA, conflicts, nil
|
||||
}
|
||||
|
||||
// runInSharedRepo is helper function used to run the provided function inside a shared repository.
|
||||
func runInSharedRepo(
|
||||
ctx context.Context,
|
||||
tmpDir, repoPath string,
|
||||
fn func(s *sharedrepo.SharedRepo) error,
|
||||
) error {
|
||||
s, err := sharedrepo.NewSharedRepo(tmpDir, repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer s.Close(ctx)
|
||||
|
||||
err = s.InitAsBare(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fn(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.MoveObjects(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,23 +15,20 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/harness/gitness/git/sharedrepo"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFilePermission = "100644" // 0o644 default file permission
|
||||
filePermissionDefault = "100644"
|
||||
)
|
||||
|
||||
type FileAction string
|
||||
|
@ -52,7 +49,7 @@ type CommitFileAction struct {
|
|||
Action FileAction
|
||||
Path string
|
||||
Payload []byte
|
||||
SHA string
|
||||
SHA sha.SHA
|
||||
}
|
||||
|
||||
// CommitFilesParams holds the data for file operations.
|
||||
|
@ -92,8 +89,6 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
return CommitFilesResponse{}, err
|
||||
}
|
||||
|
||||
log := log.Ctx(ctx).With().Str("repo_uid", params.RepoUID).Logger()
|
||||
|
||||
committer := params.Actor
|
||||
if params.Committer != nil {
|
||||
committer = *params.Committer
|
||||
|
@ -114,9 +109,8 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
|
||||
log.Debug().Msg("check if empty")
|
||||
|
||||
// check if repo is empty
|
||||
|
||||
// IMPORTANT: we don't use gitea's repo.IsEmpty() as that only checks whether the default branch exists (in HEAD).
|
||||
// This can be an issue in case someone created a branch already in the repo (just default branch is missing).
|
||||
// In that case the user can accidentally create separate git histories (which most likely is unintended).
|
||||
|
@ -126,7 +120,7 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to determine if repository is empty: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("validate and prepare input")
|
||||
// validate and prepare input
|
||||
|
||||
// ensure input data is valid
|
||||
// the commit will be nil for empty repositories
|
||||
|
@ -135,50 +129,46 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
return CommitFilesResponse{}, err
|
||||
}
|
||||
|
||||
// ref updater
|
||||
|
||||
var oldCommitSHA sha.SHA
|
||||
if commit != nil {
|
||||
var newCommitSHA sha.SHA
|
||||
|
||||
branchRef := api.GetReferenceFromBranchName(params.Branch)
|
||||
if params.Branch != params.NewBranch {
|
||||
// we are creating a new branch, rather than updating the existing one
|
||||
oldCommitSHA = sha.Nil
|
||||
branchRef = api.GetReferenceFromBranchName(params.NewBranch)
|
||||
} else if commit != nil {
|
||||
oldCommitSHA = commit.SHA
|
||||
}
|
||||
|
||||
log.Debug().Msg("create shared repo")
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
|
||||
|
||||
newCommitSHA, err := func() (sha.SHA, error) {
|
||||
// Create a directory for the temporary shared repository.
|
||||
shared, err := api.NewSharedRepo(s.git, s.tmpDir, params.RepoUID, repoPath)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to create shared repository: %w", err)
|
||||
}
|
||||
defer shared.Close(ctx)
|
||||
// run the actions in a shared repo
|
||||
|
||||
// Create bare repository with alternates pointing to the original repository.
|
||||
err = shared.InitAsShared(ctx)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to create temp repo with alternates: %w", err)
|
||||
}
|
||||
err = sharedrepo.Run(ctx, refUpdater, s.tmpDir, repoPath, func(r *sharedrepo.SharedRepo) error {
|
||||
var parentCommits []sha.SHA
|
||||
|
||||
log.Debug().Msgf("prepare tree (empty: %t)", isEmpty)
|
||||
|
||||
// handle empty repo separately (as branch doesn't exist, no commit exists, ...)
|
||||
if isEmpty {
|
||||
err = s.prepareTreeEmptyRepo(ctx, shared, params.Actions)
|
||||
err = s.prepareTreeEmptyRepo(ctx, r, params.Actions)
|
||||
} else {
|
||||
err = shared.SetIndex(ctx, oldCommitSHA.String())
|
||||
parentCommits = append(parentCommits, commit.SHA)
|
||||
|
||||
err = r.SetIndex(ctx, commit.SHA)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to set index to temp repo: %w", err)
|
||||
return fmt.Errorf("failed to set index in shared repository: %w", err)
|
||||
}
|
||||
|
||||
err = s.prepareTree(ctx, shared, params.Actions, commit)
|
||||
err = s.prepareTree(ctx, r, commit.SHA, params.Actions)
|
||||
}
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to prepare tree: %w", err)
|
||||
return fmt.Errorf("failed to prepare tree: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("write tree")
|
||||
|
||||
// Now write the tree
|
||||
treeHash, err := shared.WriteTree(ctx)
|
||||
treeHash, err := r.WriteTree(ctx)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to write tree object: %w", err)
|
||||
return fmt.Errorf("failed to write tree object: %w", err)
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(params.Title)
|
||||
|
@ -186,62 +176,40 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
message += "\n\n" + strings.TrimSpace(params.Message)
|
||||
}
|
||||
|
||||
log.Debug().Msg("commit tree")
|
||||
|
||||
// Now commit the tree
|
||||
commitSHA, err := shared.CommitTreeWithDate(
|
||||
ctx,
|
||||
oldCommitSHA,
|
||||
&api.Identity{
|
||||
authorSig := &api.Signature{
|
||||
Identity: api.Identity{
|
||||
Name: author.Name,
|
||||
Email: author.Email,
|
||||
},
|
||||
&api.Identity{
|
||||
When: authorDate,
|
||||
}
|
||||
|
||||
committerSig := &api.Signature{
|
||||
Identity: api.Identity{
|
||||
Name: committer.Name,
|
||||
Email: committer.Email,
|
||||
},
|
||||
treeHash,
|
||||
message,
|
||||
false,
|
||||
authorDate,
|
||||
committerDate,
|
||||
)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to commit the tree: %w", err)
|
||||
When: committerDate,
|
||||
}
|
||||
|
||||
err = shared.MoveObjects(ctx)
|
||||
commitSHA, err := r.CommitTree(ctx, authorSig, committerSig, treeHash, message, false, parentCommits...)
|
||||
if err != nil {
|
||||
return sha.None, fmt.Errorf("failed to move git objects: %w", err)
|
||||
return fmt.Errorf("failed to commit the tree: %w", err)
|
||||
}
|
||||
|
||||
return commitSHA, nil
|
||||
}()
|
||||
newCommitSHA = commitSHA
|
||||
|
||||
if err := refUpdater.Init(ctx, oldCommitSHA, newCommitSHA); err != nil {
|
||||
return fmt.Errorf("failed to init ref updater old=%s new=%s: %w", oldCommitSHA, newCommitSHA, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to create commit in shared repository: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("update ref")
|
||||
|
||||
branchRef := api.GetReferenceFromBranchName(params.Branch)
|
||||
if params.Branch != params.NewBranch {
|
||||
// we are creating a new branch, rather than updating the existing one
|
||||
oldCommitSHA = sha.Nil
|
||||
branchRef = api.GetReferenceFromBranchName(params.NewBranch)
|
||||
}
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
branchRef,
|
||||
oldCommitSHA,
|
||||
newCommitSHA,
|
||||
)
|
||||
if err != nil {
|
||||
return CommitFilesResponse{}, fmt.Errorf("CommitFiles: failed to update ref %s: %w", branchRef, err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("get commit")
|
||||
// get commit
|
||||
|
||||
commit, err = s.git.GetCommit(ctx, repoPath, newCommitSHA.String())
|
||||
if err != nil {
|
||||
|
@ -249,8 +217,6 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
newCommitSHA.String(), err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("done")
|
||||
|
||||
return CommitFilesResponse{
|
||||
CommitID: commit.SHA,
|
||||
}, nil
|
||||
|
@ -258,13 +224,13 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
|
|||
|
||||
func (s *Service) prepareTree(
|
||||
ctx context.Context,
|
||||
shared *api.SharedRepo,
|
||||
r *sharedrepo.SharedRepo,
|
||||
treeish sha.SHA,
|
||||
actions []CommitFileAction,
|
||||
commit *api.Commit,
|
||||
) error {
|
||||
// execute all actions
|
||||
for i := range actions {
|
||||
if err := s.processAction(ctx, shared, &actions[i], commit); err != nil {
|
||||
if err := s.processAction(ctx, r, treeish, &actions[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -274,7 +240,7 @@ func (s *Service) prepareTree(
|
|||
|
||||
func (s *Service) prepareTreeEmptyRepo(
|
||||
ctx context.Context,
|
||||
shared *api.SharedRepo,
|
||||
r *sharedrepo.SharedRepo,
|
||||
actions []CommitFileAction,
|
||||
) error {
|
||||
for _, action := range actions {
|
||||
|
@ -287,7 +253,7 @@ func (s *Service) prepareTreeEmptyRepo(
|
|||
return errors.InvalidArgument("invalid path")
|
||||
}
|
||||
|
||||
if err := createFile(ctx, shared, nil, filePath, defaultFilePermission, action.Payload); err != nil {
|
||||
if err := r.CreateFile(ctx, sha.None, filePath, filePermissionDefault, action.Payload); err != nil {
|
||||
return errors.Internal(err, "failed to create file '%s'", action.Path)
|
||||
}
|
||||
}
|
||||
|
@ -342,6 +308,71 @@ func (s *Service) validateAndPrepareHeader(
|
|||
return branch.Commit, nil
|
||||
}
|
||||
|
||||
func (s *Service) processAction(
|
||||
ctx context.Context,
|
||||
r *sharedrepo.SharedRepo,
|
||||
treeishSHA sha.SHA,
|
||||
action *CommitFileAction,
|
||||
) (err error) {
|
||||
filePath := api.CleanUploadFileName(action.Path)
|
||||
if filePath == "" {
|
||||
return errors.InvalidArgument("path cannot be empty")
|
||||
}
|
||||
|
||||
switch action.Action {
|
||||
case CreateAction:
|
||||
err = r.CreateFile(ctx, treeishSHA, filePath, filePermissionDefault, action.Payload)
|
||||
case UpdateAction:
|
||||
err = r.UpdateFile(ctx, treeishSHA, filePath, action.SHA, filePermissionDefault, action.Payload)
|
||||
case MoveAction:
|
||||
err = r.MoveFile(ctx, treeishSHA, filePath, action.SHA, filePermissionDefault, action.Payload)
|
||||
case DeleteAction:
|
||||
err = r.DeleteFile(ctx, filePath)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
func (s *Service) prepareTree(
|
||||
ctx context.Context,
|
||||
shared *api.SharedRepo,
|
||||
actions []CommitFileAction,
|
||||
commit *api.Commit,
|
||||
) error {
|
||||
// execute all actions
|
||||
for i := range actions {
|
||||
if err := s.processAction(ctx, shared, &actions[i], commit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareTreeEmptyRepo(
|
||||
ctx context.Context,
|
||||
shared *api.SharedRepo,
|
||||
actions []CommitFileAction,
|
||||
) error {
|
||||
for _, action := range actions {
|
||||
if action.Action != CreateAction {
|
||||
return errors.PreconditionFailed("action not allowed on empty repository")
|
||||
}
|
||||
|
||||
filePath := api.CleanUploadFileName(action.Path)
|
||||
if filePath == "" {
|
||||
return errors.InvalidArgument("invalid path")
|
||||
}
|
||||
|
||||
if err := createFile(ctx, shared, nil, filePath, defaultFilePermission, action.Payload); err != nil {
|
||||
return errors.Internal(err, "failed to create file '%s'", action.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) processAction(
|
||||
ctx context.Context,
|
||||
shared *api.SharedRepo,
|
||||
|
@ -571,3 +602,4 @@ func parseMovePayload(payload []byte) (string, []byte, error) {
|
|||
|
||||
return newPath, newContent, nil
|
||||
}
|
||||
*/
|
||||
|
|
18
git/ref.go
18
git/ref.go
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
|
@ -60,12 +61,12 @@ func (s *Service) GetRef(ctx context.Context, params GetRefParams) (GetRefRespon
|
|||
return GetRefResponse{}, fmt.Errorf("GetRef: failed to fetch reference '%s': %w", params.Name, err)
|
||||
}
|
||||
|
||||
sha, err := s.git.GetRef(ctx, repoPath, reference)
|
||||
refSHA, err := s.git.GetRef(ctx, repoPath, reference)
|
||||
if err != nil {
|
||||
return GetRefResponse{}, err
|
||||
}
|
||||
|
||||
return GetRefResponse{SHA: sha}, nil
|
||||
return GetRefResponse{SHA: refSHA}, nil
|
||||
}
|
||||
|
||||
type UpdateRefParams struct {
|
||||
|
@ -91,15 +92,12 @@ func (s *Service) UpdateRef(ctx context.Context, params UpdateRefParams) error {
|
|||
return fmt.Errorf("UpdateRef: failed to fetch reference '%s': %w", params.Name, err)
|
||||
}
|
||||
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
reference,
|
||||
params.OldValue,
|
||||
params.NewValue,
|
||||
)
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, reference)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRef: failed to create ref updater: %w", err)
|
||||
}
|
||||
|
||||
if err := refUpdater.Do(ctx, params.OldValue, params.NewValue); err != nil {
|
||||
return fmt.Errorf("failed to update ref: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/sharedrepo"
|
||||
)
|
||||
|
||||
type ScanSecretsParams struct {
|
||||
|
@ -39,20 +40,13 @@ func (s *Service) ScanSecrets(ctx context.Context, params *ScanSecretsParams) (*
|
|||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
|
||||
// Create a directory for the temporary shared repository.
|
||||
shared, err := api.NewSharedRepo(s.git, s.tmpDir, params.RepoUID, repoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create shared repository: %w", err)
|
||||
}
|
||||
defer shared.Close(ctx)
|
||||
var findings []api.Finding
|
||||
|
||||
// Create bare repository with alternates pointing to the original repository.
|
||||
err = shared.InitAsSharedWithAlternates(ctx, params.AlternateObjectDirs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp repo with alternates: %w", err)
|
||||
}
|
||||
|
||||
findings, err := s.git.ScanSecrets(shared.RepoPath, params.BaseRev, params.Rev)
|
||||
err := sharedrepo.Run(ctx, nil, s.tmpDir, repoPath, func(sharedRepo *sharedrepo.SharedRepo) error {
|
||||
var err error
|
||||
findings, err = s.git.ScanSecrets(sharedRepo.Directory(), params.BaseRev, params.Rev)
|
||||
return err
|
||||
}, params.AlternateObjectDirs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get leaks on diff: %w", err)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/storage"
|
||||
"github.com/harness/gitness/git/types"
|
||||
)
|
||||
|
@ -30,17 +31,19 @@ const (
|
|||
)
|
||||
|
||||
type Service struct {
|
||||
reposRoot string
|
||||
tmpDir string
|
||||
git *api.Git
|
||||
store storage.Store
|
||||
gitHookPath string
|
||||
reposGraveyard string
|
||||
reposRoot string
|
||||
tmpDir string
|
||||
git *api.Git
|
||||
hookClientFactory hook.ClientFactory
|
||||
store storage.Store
|
||||
gitHookPath string
|
||||
reposGraveyard string
|
||||
}
|
||||
|
||||
func New(
|
||||
config types.Config,
|
||||
adapter *api.Git,
|
||||
hookClientFactory hook.ClientFactory,
|
||||
storage storage.Store,
|
||||
) (*Service, error) {
|
||||
// Create repos folder
|
||||
|
@ -60,11 +63,12 @@ func New(
|
|||
}
|
||||
}
|
||||
return &Service{
|
||||
reposRoot: reposRoot,
|
||||
tmpDir: config.TmpDir,
|
||||
reposGraveyard: reposGraveyard,
|
||||
git: adapter,
|
||||
store: storage,
|
||||
gitHookPath: config.HookPath,
|
||||
reposRoot: reposRoot,
|
||||
tmpDir: config.TmpDir,
|
||||
reposGraveyard: reposGraveyard,
|
||||
git: adapter,
|
||||
hookClientFactory: hookClientFactory,
|
||||
store: storage,
|
||||
gitHookPath: config.HookPath,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/git/hook"
|
||||
)
|
||||
|
||||
// Run is helper function used to run the provided function inside a shared repository.
|
||||
// If the provided hook.RefUpdater is not nil it will be used to update the reference.
|
||||
// Inside the provided inline function there should be a call to initialize the ref updater.
|
||||
// If the provided hook.RefUpdater is nil the entire operation is a read-only.
|
||||
func Run(
|
||||
ctx context.Context,
|
||||
refUpdater *hook.RefUpdater,
|
||||
tmpDir, repoPath string,
|
||||
fn func(s *SharedRepo) error,
|
||||
alternates ...string,
|
||||
) error {
|
||||
s, err := NewSharedRepo(tmpDir, repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer s.Close(ctx)
|
||||
|
||||
if err := s.Init(ctx, alternates...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The refUpdater.Init must be called within the fn (if refUpdater != nil), otherwise the refUpdater.Pre will fail.
|
||||
if err := fn(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if refUpdater == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
alternate := s.Directory() + "/objects"
|
||||
|
||||
if err := refUpdater.Pre(ctx, alternate); err != nil {
|
||||
return fmt.Errorf("pre-receive hook failed: %w", err)
|
||||
}
|
||||
|
||||
if err := s.MoveObjects(ctx); err != nil {
|
||||
return fmt.Errorf("failed to move objects: %w", err)
|
||||
}
|
||||
|
||||
if err := refUpdater.UpdateRef(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update reference: %w", err)
|
||||
}
|
||||
|
||||
if err := refUpdater.Post(ctx, alternate); err != nil {
|
||||
return fmt.Errorf("post-receive hook failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -37,64 +37,75 @@ import (
|
|||
"github.com/harness/gitness/git/tempdir"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type SharedRepo struct {
|
||||
temporaryPath string
|
||||
repositoryPath string
|
||||
repoPath string
|
||||
sourceRepoPath string
|
||||
}
|
||||
|
||||
// NewSharedRepo creates a new temporary bare repository.
|
||||
func NewSharedRepo(
|
||||
baseTmpDir string,
|
||||
repositoryPath string,
|
||||
sourceRepoPath string,
|
||||
) (*SharedRepo, error) {
|
||||
if sourceRepoPath == "" {
|
||||
return nil, errors.New("repository path can't be empty")
|
||||
}
|
||||
|
||||
var buf [5]byte
|
||||
_, _ = rand.Read(buf[:])
|
||||
id := base32.StdEncoding.EncodeToString(buf[:])
|
||||
|
||||
temporaryPath, err := tempdir.CreateTemporaryPath(baseTmpDir, id)
|
||||
repoPath, 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,
|
||||
repoPath: repoPath,
|
||||
sourceRepoPath: sourceRepoPath,
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) Close(ctx context.Context) {
|
||||
if err := tempdir.RemoveTemporaryPath(r.temporaryPath); err != nil {
|
||||
if err := tempdir.RemoveTemporaryPath(r.repoPath); err != nil {
|
||||
log.Ctx(ctx).Err(err).
|
||||
Str("path", r.temporaryPath).
|
||||
Str("path", r.repoPath).
|
||||
Msg("Failed to remove temporary shared directory")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SharedRepo) InitAsBare(ctx context.Context) error {
|
||||
func (r *SharedRepo) Init(ctx context.Context, alternates ...string) error {
|
||||
cmd := command.New("init", command.WithFlag("--bare"))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath)); 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)
|
||||
alternatesFilePath := filepath.Join(r.repoPath, "objects", "info", "alternates")
|
||||
f, err := os.OpenFile(alternatesFilePath, 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")
|
||||
data := filepath.Join(r.sourceRepoPath, "objects")
|
||||
if _, err = fmt.Fprintln(f, data); err != nil {
|
||||
return fmt.Errorf("failed to write alternates file: %w", err)
|
||||
}
|
||||
|
||||
for _, alternate := range alternates {
|
||||
if _, err = fmt.Fprintln(f, alternate); 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)
|
||||
|
@ -104,14 +115,14 @@ func (r *SharedRepo) InitAsBare(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (r *SharedRepo) Directory() string {
|
||||
return r.temporaryPath
|
||||
return r.repoPath
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath)); err != nil {
|
||||
return fmt.Errorf("failed to initialize shared repository index to HEAD: %w", err)
|
||||
}
|
||||
|
||||
|
@ -119,10 +130,10 @@ func (r *SharedRepo) SetDefaultIndex(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// 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))
|
||||
func (r *SharedRepo) SetIndex(ctx context.Context, treeish sha.SHA) error {
|
||||
cmd := command.New("read-tree", command.WithArg(treeish.String()))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath)); err != nil {
|
||||
return fmt.Errorf("failed to initialize shared repository index to %q: %w", treeish, err)
|
||||
}
|
||||
|
||||
|
@ -133,7 +144,7 @@ func (r *SharedRepo) SetIndex(ctx context.Context, treeish string) error {
|
|||
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 {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath)); err != nil {
|
||||
return fmt.Errorf("failed to clear shared repository index: %w", err)
|
||||
}
|
||||
|
||||
|
@ -152,7 +163,7 @@ func (r *SharedRepo) LsFiles(
|
|||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout))
|
||||
err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdout(stdout))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list files in shared repository's git index: %w", err)
|
||||
}
|
||||
|
@ -184,7 +195,7 @@ func (r *SharedRepo) RemoveFilesFromIndex(
|
|||
}
|
||||
}
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdin(stdin)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdin(stdin)); err != nil {
|
||||
return fmt.Errorf("failed to update-index in shared repo: %w", err)
|
||||
}
|
||||
|
||||
|
@ -195,7 +206,7 @@ func (r *SharedRepo) RemoveFilesFromIndex(
|
|||
func (r *SharedRepo) WriteGitObject(
|
||||
ctx context.Context,
|
||||
content io.Reader,
|
||||
) (string, error) {
|
||||
) (sha.SHA, error) {
|
||||
cmd := command.New("hash-object",
|
||||
command.WithFlag("-w"),
|
||||
command.WithFlag("--stdin"))
|
||||
|
@ -203,14 +214,14 @@ func (r *SharedRepo) WriteGitObject(
|
|||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.temporaryPath),
|
||||
command.WithDir(r.repoPath),
|
||||
command.WithStdin(content),
|
||||
command.WithStdout(stdout))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash-object in shared repo: %w", err)
|
||||
return sha.None, fmt.Errorf("failed to hash-object in shared repo: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
// GetTreeSHA returns the tree SHA of the rev.
|
||||
|
@ -225,7 +236,7 @@ func (r *SharedRepo) GetTreeSHA(
|
|||
)
|
||||
stdout := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.temporaryPath),
|
||||
command.WithDir(r.repoPath),
|
||||
command.WithStdout(stdout),
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -249,7 +260,7 @@ func (r *SharedRepo) ShowFile(
|
|||
|
||||
cmd := command.New("show", command.WithArg(file))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(writer)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdout(writer)); err != nil {
|
||||
return fmt.Errorf("failed to show file in shared repo: %w", err)
|
||||
}
|
||||
|
||||
|
@ -260,15 +271,15 @@ func (r *SharedRepo) ShowFile(
|
|||
func (r *SharedRepo) AddObjectToIndex(
|
||||
ctx context.Context,
|
||||
mode string,
|
||||
objectHash string,
|
||||
objectHash sha.SHA,
|
||||
objectPath string,
|
||||
) error {
|
||||
cmd := command.New("update-index",
|
||||
command.WithFlag("--add"),
|
||||
command.WithFlag("--replace"),
|
||||
command.WithFlag("--cacheinfo", mode, objectHash, objectPath))
|
||||
command.WithFlag("--cacheinfo", mode, objectHash.String(), objectPath))
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath)); err != nil {
|
||||
if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Error()); matched {
|
||||
return errors.InvalidArgument("invalid path '%s'", objectPath)
|
||||
}
|
||||
|
@ -279,16 +290,16 @@ func (r *SharedRepo) AddObjectToIndex(
|
|||
}
|
||||
|
||||
// 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) {
|
||||
func (r *SharedRepo) WriteTree(ctx context.Context) (sha.SHA, 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)
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdout(stdout)); err != nil {
|
||||
return sha.None, fmt.Errorf("failed to write-tree in shared repo: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
return sha.New(stdout.String())
|
||||
}
|
||||
|
||||
// MergeTree merges commits in git index.
|
||||
|
@ -310,7 +321,7 @@ func (r *SharedRepo) MergeTree(
|
|||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.temporaryPath),
|
||||
command.WithDir(r.repoPath),
|
||||
command.WithStdout(stdout))
|
||||
|
||||
// no error: the output is just the tree object SHA
|
||||
|
@ -376,7 +387,7 @@ func (r *SharedRepo) CommitTree(
|
|||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(r.temporaryPath),
|
||||
command.WithDir(r.repoPath),
|
||||
command.WithStdout(stdout),
|
||||
command.WithStdin(messageBytes))
|
||||
if err != nil {
|
||||
|
@ -405,7 +416,7 @@ func (r *SharedRepo) CommitSHAsForRebase(
|
|||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdout(stdout)); err != nil {
|
||||
return nil, fmt.Errorf("failed to rev-list in shared repo: %w", err)
|
||||
}
|
||||
|
||||
|
@ -432,17 +443,214 @@ func (r *SharedRepo) MergeBase(
|
|||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
if err := cmd.Run(ctx, command.WithDir(r.temporaryPath), command.WithStdout(stdout)); err != nil {
|
||||
if err := cmd.Run(ctx, command.WithDir(r.repoPath), command.WithStdout(stdout)); err != nil {
|
||||
return "", fmt.Errorf("failed to merge-base in shared repo: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) CreateFile(
|
||||
ctx context.Context,
|
||||
treeishSHA sha.SHA,
|
||||
filePath, mode string,
|
||||
payload []byte,
|
||||
) error {
|
||||
// only check path availability if a source commit is available (empty repo won't have such a commit)
|
||||
if !treeishSHA.IsEmpty() {
|
||||
if err := r.checkPathAvailability(ctx, treeishSHA, filePath, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
objectSHA, err := r.WriteGitObject(ctx, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("createFile: error hashing object: %w", err)
|
||||
}
|
||||
|
||||
// Add the object to the index
|
||||
if err = r.AddObjectToIndex(ctx, mode, objectSHA, filePath); err != nil {
|
||||
return fmt.Errorf("createFile: error creating object: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) UpdateFile(
|
||||
ctx context.Context,
|
||||
treeishSHA sha.SHA,
|
||||
filePath string,
|
||||
objectSHA sha.SHA,
|
||||
mode string,
|
||||
payload []byte,
|
||||
) error {
|
||||
// get file mode from existing file (default unless executable)
|
||||
entry, err := r.getFileEntry(ctx, treeishSHA, objectSHA, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.IsExecutable() {
|
||||
mode = "100755"
|
||||
}
|
||||
|
||||
objectSHA, err = r.WriteGitObject(ctx, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("updateFile: error hashing object: %w", err)
|
||||
}
|
||||
|
||||
if err = r.AddObjectToIndex(ctx, mode, objectSHA, filePath); err != nil {
|
||||
return fmt.Errorf("updateFile: error updating object: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) MoveFile(
|
||||
ctx context.Context,
|
||||
treeishSHA sha.SHA,
|
||||
filePath string,
|
||||
objectSHA sha.SHA,
|
||||
mode string,
|
||||
payload []byte,
|
||||
) error {
|
||||
newPath, newContent, err := parseMovePayload(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure file exists and matches SHA
|
||||
entry, err := r.getFileEntry(ctx, treeishSHA, objectSHA, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure new path is available
|
||||
if err = r.checkPathAvailability(ctx, treeishSHA, newPath, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileHash sha.SHA
|
||||
var fileMode string
|
||||
if newContent != nil {
|
||||
hash, err := r.WriteGitObject(ctx, bytes.NewReader(newContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("moveFile: error hashing object: %w", err)
|
||||
}
|
||||
|
||||
fileHash = hash
|
||||
fileMode = mode
|
||||
if entry.IsExecutable() {
|
||||
const filePermissionExec = "100755"
|
||||
fileMode = filePermissionExec
|
||||
}
|
||||
} else {
|
||||
fileHash = entry.SHA
|
||||
fileMode = entry.Mode.String()
|
||||
}
|
||||
|
||||
if err = r.AddObjectToIndex(ctx, fileMode, fileHash, newPath); err != nil {
|
||||
return fmt.Errorf("moveFile: add object error: %w", err)
|
||||
}
|
||||
|
||||
if err = r.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
||||
return fmt.Errorf("moveFile: remove object error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) DeleteFile(ctx context.Context, filePath string) error {
|
||||
filesInIndex, err := r.LsFiles(ctx, filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleteFile: listing files error: %w", err)
|
||||
}
|
||||
if !slices.Contains(filesInIndex, filePath) {
|
||||
return errors.NotFound("file path %s not found", filePath)
|
||||
}
|
||||
|
||||
if err = r.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
||||
return fmt.Errorf("deleteFile: remove object error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SharedRepo) getFileEntry(
|
||||
ctx context.Context,
|
||||
treeishSHA sha.SHA,
|
||||
objectSHA sha.SHA,
|
||||
path string,
|
||||
) (*api.TreeNode, error) {
|
||||
entry, err := api.GetTreeNode(ctx, r.repoPath, treeishSHA.String(), path)
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, errors.NotFound("path %s not found", path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getFileEntry: failed to get tree for path %s: %w", path, err)
|
||||
}
|
||||
|
||||
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||
if !objectSHA.IsEmpty() && !objectSHA.Equal(entry.SHA) {
|
||||
return nil, errors.InvalidArgument("sha does not match for path %s [given: %s, expected: %s]",
|
||||
path, objectSHA, entry.SHA)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// checkPathAvailability ensures that the path is available for the requested operation.
|
||||
// For the path where this file will be created/updated, we need to make
|
||||
// sure no parts of the path are existing files or links except for the last
|
||||
// item in the path which is the file name, and that shouldn't exist IF it is
|
||||
// a new file OR is being moved to a new path.
|
||||
func (r *SharedRepo) checkPathAvailability(
|
||||
ctx context.Context,
|
||||
treeishSHA sha.SHA,
|
||||
filePath string,
|
||||
isNewFile bool,
|
||||
) error {
|
||||
parts := strings.Split(filePath, "/")
|
||||
subTreePath := ""
|
||||
|
||||
for index, part := range parts {
|
||||
subTreePath = path.Join(subTreePath, part)
|
||||
|
||||
entry, err := api.GetTreeNode(ctx, r.repoPath, treeishSHA.String(), subTreePath)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
// Means there is no item with that name, so we're good
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("checkPathAvailability: failed to get tree entry for path %s: %w", subTreePath, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case index < len(parts)-1:
|
||||
if !entry.IsDir() {
|
||||
return errors.Conflict("a file already exists where you're trying to create a subdirectory [path: %s]",
|
||||
subTreePath)
|
||||
}
|
||||
case entry.IsLink():
|
||||
return errors.Conflict("a symbolic link already exist where you're trying to create a subdirectory [path: %s]",
|
||||
subTreePath)
|
||||
case entry.IsDir():
|
||||
return errors.Conflict("a directory already exists where you're trying to create a subdirectory [path: %s]",
|
||||
subTreePath)
|
||||
case filePath != "" || isNewFile:
|
||||
return errors.Conflict("file path %s already exists", filePath)
|
||||
}
|
||||
}
|
||||
return 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")
|
||||
if r.sourceRepoPath == "" {
|
||||
return errors.New("shared repo not initialized with a repository")
|
||||
}
|
||||
|
||||
srcDir := path.Join(r.repoPath, "objects")
|
||||
dstDir := path.Join(r.sourceRepoPath, "objects")
|
||||
|
||||
var files []fileEntry
|
||||
|
||||
|
@ -565,3 +773,23 @@ type fileEntry struct {
|
|||
relPath string
|
||||
priority int
|
||||
}
|
||||
|
||||
func parseMovePayload(payload []byte) (string, []byte, error) {
|
||||
var newContent []byte
|
||||
var newPath string
|
||||
filePathEnd := bytes.IndexByte(payload, 0)
|
||||
if filePathEnd < 0 {
|
||||
newPath = string(payload)
|
||||
newContent = nil
|
||||
} else {
|
||||
newPath = string(payload[:filePathEnd])
|
||||
newContent = payload[filePathEnd+1:]
|
||||
}
|
||||
|
||||
newPath = api.CleanUploadFileName(newPath)
|
||||
if newPath == "" {
|
||||
return "", nil, api.ErrInvalidPath
|
||||
}
|
||||
|
||||
return newPath, newContent, nil
|
||||
}
|
||||
|
|
106
git/tag.go
106
git/tag.go
|
@ -21,7 +21,9 @@ import (
|
|||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/git/sharedrepo"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -252,76 +254,58 @@ func (s *Service) CreateCommitTag(ctx context.Context, params *CreateCommitTagPa
|
|||
return nil, errors.Conflict("tag '%s' already exists", tagName)
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
// Create a directory for the temporary shared repository.
|
||||
sharedRepo, err := api.NewSharedRepo(s.git, s.tmpDir, params.RepoUID, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new shared repo: %w", err)
|
||||
}
|
||||
defer sharedRepo.Close(ctx)
|
||||
// create tag request
|
||||
|
||||
// Create bare repository with alternates pointing to the original repository.
|
||||
err = sharedRepo.InitAsShared(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp repo with alternates: %w", err)
|
||||
}
|
||||
tagger := params.Actor
|
||||
if params.Tagger != nil {
|
||||
tagger = *params.Tagger
|
||||
}
|
||||
taggerDate := time.Now().UTC()
|
||||
if params.TaggerDate != nil {
|
||||
taggerDate = *params.TaggerDate
|
||||
}
|
||||
|
||||
tagger := params.Actor
|
||||
if params.Tagger != nil {
|
||||
tagger = *params.Tagger
|
||||
}
|
||||
taggerDate := time.Now().UTC()
|
||||
if params.TaggerDate != nil {
|
||||
taggerDate = *params.TaggerDate
|
||||
}
|
||||
|
||||
createTagRequest := &api.CreateTagOptions{
|
||||
Message: params.Message,
|
||||
Tagger: api.Signature{
|
||||
Identity: api.Identity{
|
||||
Name: tagger.Name,
|
||||
Email: tagger.Email,
|
||||
},
|
||||
When: taggerDate,
|
||||
createTagRequest := &api.CreateTagOptions{
|
||||
Message: params.Message,
|
||||
Tagger: api.Signature{
|
||||
Identity: api.Identity{
|
||||
Name: tagger.Name,
|
||||
Email: tagger.Email,
|
||||
},
|
||||
}
|
||||
err = s.git.CreateTag(
|
||||
ctx,
|
||||
sharedRepo.RepoPath,
|
||||
tagName,
|
||||
targetCommit.SHA,
|
||||
createTagRequest)
|
||||
if err != nil {
|
||||
When: taggerDate,
|
||||
},
|
||||
}
|
||||
|
||||
// ref updater
|
||||
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, tagRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ref updater to create the tag: %w", err)
|
||||
}
|
||||
|
||||
// create the tag
|
||||
|
||||
err = sharedrepo.Run(ctx, refUpdater, s.tmpDir, repoPath, func(r *sharedrepo.SharedRepo) error {
|
||||
if err := s.git.CreateTag(ctx, r.Directory(), tagName, targetCommit.SHA, createTagRequest); err != nil {
|
||||
return fmt.Errorf("failed to create tag '%s': %w", tagName, err)
|
||||
}
|
||||
|
||||
tag, err = s.git.GetAnnotatedTag(ctx, sharedRepo.RepoPath, tagName)
|
||||
tag, err = s.git.GetAnnotatedTag(ctx, r.Directory(), tagName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read annotated tag after creation: %w", err)
|
||||
}
|
||||
|
||||
err = sharedRepo.MoveObjects(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to move git objects: %w", err)
|
||||
if err := refUpdater.Init(ctx, sha.Nil, tag.Sha); err != nil {
|
||||
return fmt.Errorf("failed to init ref updater: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateCommitTag: failed to create tag in shared repository: %w", err)
|
||||
}
|
||||
|
||||
err = s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
tagRef,
|
||||
sha.Nil,
|
||||
tag.Sha,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create tag reference: %w", err)
|
||||
}
|
||||
// prepare response
|
||||
|
||||
var commitTag *CommitTag
|
||||
if params.Message != "" {
|
||||
|
@ -355,19 +339,17 @@ func (s *Service) DeleteTag(ctx context.Context, params *DeleteTagParams) error
|
|||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
tagRef := api.GetReferenceFromTagName(params.Name)
|
||||
|
||||
err := s.git.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
tagRef,
|
||||
sha.None, // delete whatever is there
|
||||
sha.Nil,
|
||||
)
|
||||
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, tagRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ref updater to delete the tag: %w", err)
|
||||
}
|
||||
|
||||
err = refUpdater.Do(ctx, sha.None, sha.Nil) // delete whatever is there
|
||||
if errors.IsNotFound(err) {
|
||||
return errors.NotFound("tag %q does not exist", params.Name)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete tag reference: %w", err)
|
||||
return fmt.Errorf("failed to init ref updater: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -45,11 +45,13 @@ func ProvideGITAdapter(
|
|||
func ProvideService(
|
||||
config types.Config,
|
||||
adapter *api.Git,
|
||||
hookClientFactory hook.ClientFactory,
|
||||
storage storage.Store,
|
||||
) (Interface, error) {
|
||||
return New(
|
||||
config,
|
||||
adapter,
|
||||
hookClientFactory,
|
||||
storage,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue