mirror of
https://github.com/harness/drone.git
synced 2025-05-05 05:01:46 +08:00
[PERFORMANCE] Use Update-Ref for branch create/delete and tag deletion (#930)
This commit is contained in:
parent
0ebee9af18
commit
c0bca6eea1
@ -1,4 +1,5 @@
|
||||
GITNESS_TRACE=true
|
||||
GITNESS_WEBHOOK_ALLOW_LOOPBACK=true
|
||||
GITNESS_GIT_TRACE=true
|
||||
GITNESS_PRINCIPAL_ADMIN_PASSWORD=changeit
|
||||
GITNESS_WEBHOOK_ALLOW_LOOPBACK=true
|
||||
GITNESS_METRIC_ENABLED=false
|
@ -68,7 +68,7 @@ type Adapter interface {
|
||||
GetCommitDivergences(ctx context.Context, repoPath string,
|
||||
requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error)
|
||||
GetRef(ctx context.Context, repoPath string, reference string) (string, error)
|
||||
UpdateRef(ctx context.Context, repoPath, reference, newValue, oldValue string) error
|
||||
UpdateRef(ctx context.Context, envVars map[string]string, repoPath, reference, newValue, oldValue string) error
|
||||
CreateTemporaryRepoForPR(ctx context.Context, reposTempPath string, pr *types.PullRequest,
|
||||
baseBranch, trackingBranch string) (types.TempRepository, error)
|
||||
Merge(ctx context.Context, pr *types.PullRequest, mergeMethod enum.MergeMethod, baseBranch, trackingBranch string,
|
||||
|
@ -357,7 +357,7 @@ func (a Adapter) GetCommitDivergences(
|
||||
res := make([]types.CommitDivergence, len(requests))
|
||||
for i, req := range requests {
|
||||
res[i], err = a.getCommitDivergence(ctx, repoPath, req, max)
|
||||
if errors.Is(err, &types.NotFoundError{}) {
|
||||
if types.IsNotFoundError(err) {
|
||||
res[i] = types.CommitDivergence{Ahead: -1, Behind: -1}
|
||||
continue
|
||||
}
|
||||
|
@ -21,10 +21,12 @@ import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/git/hook"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
gitearef "code.gitea.io/gitea/modules/git/foreachref"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DefaultInstructor(
|
||||
@ -156,18 +158,18 @@ func walkGiteaReferenceParser(
|
||||
func (a Adapter) GetRef(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
reference string,
|
||||
ref string,
|
||||
) (string, error) {
|
||||
if repoPath == "" {
|
||||
return "", ErrRepositoryPathEmpty
|
||||
}
|
||||
cmd := gitea.NewCommand(ctx, "show-ref", "--verify", "-s", "--", reference)
|
||||
cmd := gitea.NewCommand(ctx, "show-ref", "--verify", "-s", "--", ref)
|
||||
stdout, _, err := cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
if err != nil {
|
||||
if err.IsExitCode(128) && strings.Contains(err.Stderr(), "not a valid ref") {
|
||||
return "", types.ErrNotFound("reference '%s' not found", reference)
|
||||
return "", types.ErrNotFound("reference %q not found", ref)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@ -180,34 +182,147 @@ func (a Adapter) GetRef(
|
||||
// (e.g `refs/heads/main` instead of `main`).
|
||||
func (a Adapter) UpdateRef(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
reference string,
|
||||
newValue string,
|
||||
ref string,
|
||||
oldValue string,
|
||||
newValue string,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
args := make([]string, 0, 4)
|
||||
args = append(args, "update-ref")
|
||||
|
||||
// don't break existing interface - user calls with empty value to delete the ref.
|
||||
if newValue == "" {
|
||||
// if newvalue is empty, delete ref
|
||||
args = append(args, "-d", reference)
|
||||
newValue = types.NilSHA
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
val, err := a.GetRef(ctx, repoPath, ref)
|
||||
if types.IsNotFoundError(err) {
|
||||
// fail in case someone tries to delete a reference that doesn't exist.
|
||||
if newValue == types.NilSHA {
|
||||
return types.ErrNotFound("reference %q not found", ref)
|
||||
}
|
||||
|
||||
oldValue = types.NilSHA
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to get current value of reference: %w", err)
|
||||
} else {
|
||||
args = append(args, reference, newValue)
|
||||
oldValue = val
|
||||
}
|
||||
}
|
||||
|
||||
// if an old value was provided, verify it matches.
|
||||
if oldValue != "" {
|
||||
args = append(args, oldValue)
|
||||
}
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
_, _, err := cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
err := a.updateRefWithHooks(
|
||||
ctx,
|
||||
envVars,
|
||||
repoPath,
|
||||
ref,
|
||||
oldValue,
|
||||
newValue,
|
||||
)
|
||||
if err != nil {
|
||||
return processGiteaErrorf(err, "update-ref failed")
|
||||
return fmt.Errorf("failed to update reference with hooks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (a Adapter) updateRefWithHooks(
|
||||
ctx context.Context,
|
||||
envVars map[string]string,
|
||||
repoPath string,
|
||||
ref string,
|
||||
oldValue string,
|
||||
newValue string,
|
||||
) error {
|
||||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
|
||||
if oldValue == "" {
|
||||
return fmt.Errorf("oldValue can't be empty")
|
||||
}
|
||||
if newValue == "" {
|
||||
return fmt.Errorf("newValue can't be empty")
|
||||
}
|
||||
if oldValue == types.NilSHA && newValue == types.NilSHA {
|
||||
return fmt.Errorf("provided values cannot be both empty")
|
||||
}
|
||||
|
||||
githookClient, err := a.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,
|
||||
},
|
||||
},
|
||||
})
|
||||
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 a.traceGit {
|
||||
log.Ctx(ctx).Trace().
|
||||
Str("git", "pre-receive").
|
||||
Msgf("pre-receive call succeeded with output:\n%s", strings.Join(out.Messages, "\n"))
|
||||
}
|
||||
|
||||
args := make([]string, 0, 4)
|
||||
args = append(args, "update-ref")
|
||||
if newValue == types.NilSHA {
|
||||
args = append(args, "-d", ref)
|
||||
} else {
|
||||
args = append(args, ref, newValue)
|
||||
}
|
||||
|
||||
args = append(args, oldValue)
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
_, _, err = cmd.RunStdString(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
if err != nil {
|
||||
return processGiteaErrorf(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,
|
||||
},
|
||||
},
|
||||
})
|
||||
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 a.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
|
||||
|
119
git/branch.go
119
git/branch.go
@ -24,7 +24,6 @@ import (
|
||||
"github.com/harness/gitness/git/check"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@ -93,80 +92,42 @@ func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams)
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
env := CreateEnvironmentForPush(ctx, params.WriteParams)
|
||||
|
||||
if err := check.BranchName(params.BranchName); err != nil {
|
||||
return nil, errors.InvalidArgument(err.Error())
|
||||
}
|
||||
|
||||
repo, err := s.adapter.OpenRepository(ctx, repoPath)
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
targetCommit, err := s.adapter.GetCommit(ctx, repoPath, strings.TrimSpace(params.Target))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open repo: %w", err)
|
||||
return nil, fmt.Errorf("failed to get target commit: %w", err)
|
||||
}
|
||||
|
||||
if ok, err := repo.IsEmpty(); ok {
|
||||
if err != nil {
|
||||
return nil, errors.Internal("failed to check if repository is empty: %v", err)
|
||||
}
|
||||
return nil, errors.InvalidArgument("branch cannot be created on empty repository")
|
||||
}
|
||||
|
||||
sharedRepo, err := s.adapter.SharedRepository(s.tmpDir, params.RepoUID, repo.Path)
|
||||
if err != nil {
|
||||
return nil, errors.Internal("failed to create new shared repo", err)
|
||||
}
|
||||
defer sharedRepo.Close(ctx)
|
||||
|
||||
// clone repo (with HEAD branch - target might be anything)
|
||||
err = sharedRepo.Clone(ctx, "")
|
||||
if err != nil {
|
||||
return nil, errors.Internal("failed to clone shared repo with branch '%s'", params.BranchName, err)
|
||||
}
|
||||
|
||||
_, err = sharedRepo.GetBranchCommit(params.BranchName)
|
||||
// return an error if branch alredy exists (push doesn't fail if it's a noop or fast forward push)
|
||||
if err == nil {
|
||||
return nil, errors.Conflict("branch '%s' already exists", params.BranchName)
|
||||
}
|
||||
if !gitea.IsErrNotExist(err) {
|
||||
return nil, errors.Internal("branch creation of '%s' failed: %w", params.BranchName, err)
|
||||
}
|
||||
|
||||
// get target commit (as target could be branch/tag/commit, and tag can't be pushed using source:destination syntax)
|
||||
targetCommit, err := s.adapter.GetCommit(ctx, sharedRepo.Path(), strings.TrimSpace(params.Target))
|
||||
if gitea.IsErrNotExist(err) {
|
||||
return nil, errors.NotFound("target '%s' doesn't exist", params.Target)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Internal("failed to get commit id for target '%s'", params.Target, err)
|
||||
}
|
||||
|
||||
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
|
||||
err = sharedRepo.PushCommitToBranch(ctx, targetCommit.SHA, params.BranchName, false, env...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get branch
|
||||
// TODO: get it from shared repo to avoid opening another gitea repo and having to strip here.
|
||||
gitBranch, err := s.adapter.GetBranch(
|
||||
branchRef := adapter.GetReferenceFromBranchName(params.BranchName)
|
||||
err = s.adapter.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
strings.TrimPrefix(params.BranchName, gitReferenceNamePrefixBranch),
|
||||
branchRef,
|
||||
types.NilSHA, // we want to make sure we don't overwrite any parallel create
|
||||
targetCommit.SHA,
|
||||
)
|
||||
if errors.IsConflict(err) {
|
||||
return nil, errors.Conflict("branch %q already exists", params.BranchName, err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get git branch '%s': %w", params.BranchName, err)
|
||||
return nil, fmt.Errorf("failed to update branch reference: %w", err)
|
||||
}
|
||||
|
||||
branch, err := mapBranch(gitBranch)
|
||||
commit, err := mapCommit(targetCommit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map rpc branch %v: %w", gitBranch.Name, err)
|
||||
return nil, fmt.Errorf("failed to map git commit: %w", err)
|
||||
}
|
||||
|
||||
return &CreateBranchOutput{
|
||||
Branch: *branch,
|
||||
Branch: Branch{
|
||||
Name: params.BranchName,
|
||||
SHA: commit.SHA,
|
||||
Commit: commit,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -199,37 +160,21 @@ func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams)
|
||||
}
|
||||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
env := CreateEnvironmentForPush(ctx, params.WriteParams)
|
||||
branchRef := adapter.GetReferenceFromBranchName(params.BranchName)
|
||||
|
||||
repo, err := s.adapter.OpenRepository(ctx, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open repo: %w", err)
|
||||
err := s.adapter.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
branchRef,
|
||||
"", // delete whatever is there
|
||||
types.NilSHA,
|
||||
)
|
||||
if types.IsNotFoundError(err) {
|
||||
return errors.NotFound("branch %q does not exist", params.BranchName, err)
|
||||
}
|
||||
|
||||
sharedRepo, err := s.adapter.SharedRepository(s.tmpDir, params.RepoUID, repo.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new shared repo: %w", err)
|
||||
}
|
||||
defer sharedRepo.Close(ctx)
|
||||
|
||||
// clone repo (technically we don't care about which branch we clone)
|
||||
err = sharedRepo.Clone(ctx, params.BranchName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone shared repo with branch '%s': %w", params.BranchName, err)
|
||||
}
|
||||
|
||||
// get latest branch commit before we delete
|
||||
_, err = sharedRepo.GetBranchCommit(params.BranchName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get gitea commit for branch '%s': %w", params.BranchName, err)
|
||||
}
|
||||
|
||||
// push to remote (all changes should go through push flow for hooks and other safety meassures)
|
||||
// NOTE: setting sourceRef to empty will delete the remote branch when pushing:
|
||||
// https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrefspecgt82308203
|
||||
err = sharedRepo.PushDeleteBranch(ctx, params.BranchName, true, env...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete branch '%s' from remote repo: %w", params.BranchName, err)
|
||||
return fmt.Errorf("failed to delete branch reference: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
51
git/ref.go
51
git/ref.go
@ -92,49 +92,16 @@ func (s *Service) UpdateRef(ctx context.Context, params UpdateRefParams) error {
|
||||
return fmt.Errorf("UpdateRef: failed to fetch reference '%s': %w", params.Name, err)
|
||||
}
|
||||
|
||||
repo, err := s.adapter.OpenRepository(ctx, repoPath)
|
||||
err = s.adapter.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
reference,
|
||||
params.OldValue,
|
||||
params.NewValue,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRef: failed to open repo: %w", err)
|
||||
}
|
||||
|
||||
if ok, _ := repo.IsEmpty(); ok {
|
||||
return errors.InvalidArgument("branch cannot be created on empty repository")
|
||||
}
|
||||
|
||||
sharedRepo, err := s.adapter.SharedRepository(s.tmpDir, params.RepoUID, repo.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRef: failed to create new shared repo: %w", err)
|
||||
}
|
||||
defer sharedRepo.Close(ctx)
|
||||
|
||||
// clone repo (with HEAD branch - target might be anything)
|
||||
err = sharedRepo.Clone(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRef: failed to clone shared repo: %w", err)
|
||||
}
|
||||
|
||||
pushOpts := types.PushOptions{
|
||||
Remote: sharedRepo.RemotePath(),
|
||||
Env: CreateEnvironmentForPush(ctx, params.WriteParams),
|
||||
}
|
||||
|
||||
// handle deletion explicitly to avoid any unwanted side effects
|
||||
if params.NewValue == "" {
|
||||
pushOpts.Branch = ":" + reference
|
||||
} else {
|
||||
pushOpts.Branch = params.NewValue + ":" + reference
|
||||
}
|
||||
|
||||
if params.OldValue == "" {
|
||||
pushOpts.Force = true
|
||||
} else {
|
||||
pushOpts.ForceWithLease = reference + ":" + params.OldValue
|
||||
}
|
||||
|
||||
// TODO: our shared repo has so much duplication, that should be changed IMHO.
|
||||
err = s.adapter.Push(ctx, sharedRepo.Path(), pushOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateRef: failed to push changes to original repo: %w", err)
|
||||
return fmt.Errorf("failed to update ref: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
33
git/tag.go
33
git/tag.go
@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/adapter"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -224,6 +225,7 @@ func (s *Service) ListCommitTags(
|
||||
Tags: tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateCommitTag(ctx context.Context, params *CreateCommitTagParams) (*CreateCommitTagOutput, error) {
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, err
|
||||
@ -334,28 +336,21 @@ func (s *Service) DeleteTag(ctx context.Context, params *DeleteTagParams) error
|
||||
}
|
||||
|
||||
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
||||
tagRef := adapter.GetReferenceFromTagName(params.Name)
|
||||
|
||||
repo, err := s.adapter.OpenRepository(ctx, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DeleteTag: failed to open repo: %w", err)
|
||||
err := s.adapter.UpdateRef(
|
||||
ctx,
|
||||
params.EnvVars,
|
||||
repoPath,
|
||||
tagRef,
|
||||
"", // delete whatever is there
|
||||
types.NilSHA,
|
||||
)
|
||||
if types.IsNotFoundError(err) {
|
||||
return errors.NotFound("tag %q does not exist", params.Name, err)
|
||||
}
|
||||
|
||||
sharedRepo, err := s.adapter.SharedRepository(s.tmpDir, params.RepoUID, repo.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DeleteTag: failed to create new shared repo: %w", err)
|
||||
}
|
||||
|
||||
defer sharedRepo.Close(ctx)
|
||||
|
||||
// clone repo (with HEAD branch - tag target might be anything)
|
||||
err = sharedRepo.Clone(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("DeleteTag: failed to clone shared repo with tag '%s': %w",
|
||||
params.Name, err)
|
||||
}
|
||||
envs := CreateEnvironmentForPush(ctx, params.WriteParams)
|
||||
if err = sharedRepo.PushDeleteTag(ctx, params.Name, true, envs...); err != nil {
|
||||
return fmt.Errorf("DeleteTag: Failed to push the tag %s to remote: %w", params.Name, err)
|
||||
return fmt.Errorf("failed to delete tag reference: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -37,10 +37,20 @@ type NotFoundError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func IsNotFoundError(err error) bool {
|
||||
return errors.Is(err, &NotFoundError{})
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
//nolint:errorlint // the purpose of this method is to check whether the target itself if of this type.
|
||||
func (e *NotFoundError) Is(target error) bool {
|
||||
_, ok := target.(*NotFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func ErrNotFound(format string, args ...any) error {
|
||||
return &NotFoundError{
|
||||
Msg: fmt.Sprintf(format, args...),
|
||||
|
@ -22,6 +22,8 @@ import (
|
||||
"github.com/harness/gitness/errors"
|
||||
)
|
||||
|
||||
const NilSHA = "0000000000000000000000000000000000000000"
|
||||
|
||||
type CloneRepoOptions struct {
|
||||
Timeout time.Duration
|
||||
Mirror bool
|
||||
|
Loading…
Reference in New Issue
Block a user