refactor git hook calling and shared repo (#1200)

code-1625
Marko Gacesa 2024-04-12 09:48:35 +00:00 committed by Harness
parent 740d47529a
commit 781b3547c0
22 changed files with 916 additions and 1112 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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.

285
git/hook/refupdate.go Normal file
View File

@ -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())
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
*/

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

74
git/sharedrepo/run.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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,
)
}