feat: [CODE-3076]: update several refs on merge (#3355)

* resolve pr comments
* update several refs on merge
This commit is contained in:
Marko Gaćeša 2025-02-03 10:52:01 +00:00 committed by Harness
parent 898ecb123c
commit 530a707052
15 changed files with 432 additions and 413 deletions

View File

@ -177,13 +177,7 @@ func (c *Controller) Merge(
} }
sourceRepo := targetRepo sourceRepo := targetRepo
sourceWriteParams := targetWriteParams
if pr.SourceRepoID != pr.TargetRepoID { if pr.SourceRepoID != pr.TargetRepoID {
sourceWriteParams, err = controller.CreateRPCInternalWriteParams(ctx, c.urlProvider, session, sourceRepo)
if err != nil {
return nil, nil, fmt.Errorf("failed to create RPC write params: %w", err)
}
sourceRepo, err = c.repoStore.Find(ctx, pr.SourceRepoID) sourceRepo, err = c.repoStore.Find(ctx, pr.SourceRepoID)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to get source repository: %w", err) return nil, nil, fmt.Errorf("failed to get source repository: %w", err)
@ -195,7 +189,7 @@ func (c *Controller) Merge(
return nil, nil, fmt.Errorf("failed to fetch rules: %w", err) return nil, nil, fmt.Errorf("failed to fetch rules: %w", err)
} }
checkResults, err := c.checkStore.ListResults(ctx, targetRepo.ID, pr.SourceSHA) checkResults, err := c.checkStore.ListResults(ctx, targetRepo.ID, in.SourceSHA)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to list status checks: %w", err) return nil, nil, fmt.Errorf("failed to list status checks: %w", err)
} }
@ -292,7 +286,7 @@ func (c *Controller) Merge(
BaseBranch: pr.TargetBranch, BaseBranch: pr.TargetBranch,
HeadRepoUID: sourceRepo.GitUID, HeadRepoUID: sourceRepo.GitUID,
HeadBranch: pr.SourceBranch, HeadBranch: pr.SourceBranch,
RefType: gitenum.RefTypeUndefined, // update no refs -> no commit will be created Refs: nil, // update no refs -> no commit will be created
HeadExpectedSHA: sha.Must(in.SourceSHA), HeadExpectedSHA: sha.Must(in.SourceSHA),
Method: gitenum.MergeMethod(in.Method), Method: gitenum.MergeMethod(in.Method),
}) })
@ -425,6 +419,65 @@ func (c *Controller) Merge(
log.Ctx(ctx).Debug().Msgf("all pre-check passed, merge PR") log.Ctx(ctx).Debug().Msgf("all pre-check passed, merge PR")
sourceBranchSHA, err := sha.New(in.SourceSHA)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert source SHA: %w", err)
}
refSourceBranch, err := git.GetRefPath(pr.SourceBranch, gitenum.RefTypeBranch)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate source branch ref name: %w", err)
}
refTargetBranch, err := git.GetRefPath(pr.TargetBranch, gitenum.RefTypeBranch)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate target branch ref name: %w", err)
}
prNumber := strconv.FormatInt(pr.Number, 10)
refPullReqHead, err := git.GetRefPath(prNumber, gitenum.RefTypePullReqHead)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate pull request head ref name: %w", err)
}
refPullReqMerge, err := git.GetRefPath(prNumber, gitenum.RefTypePullReqMerge)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate pull requert merge ref name: %w", err)
}
refUpdates := make([]git.RefUpdate, 0, 4)
// Update the target branch to the result of the merge.
refUpdates = append(refUpdates, git.RefUpdate{
Name: refTargetBranch,
Old: sha.SHA{}, // don't care about the current commit SHA of the target branch.
New: sha.SHA{}, // update to the result of the merge.
})
// Make sure the PR head ref points to the correct commit after the merge.
refUpdates = append(refUpdates, git.RefUpdate{
Name: refPullReqHead,
Old: sha.SHA{}, // don't care about the old value.
New: sourceBranchSHA,
})
// Delete the PR merge reference.
refUpdates = append(refUpdates, git.RefUpdate{
Name: refPullReqMerge,
Old: sha.SHA{},
New: sha.Nil,
})
if ruleOut.DeleteSourceBranch {
// Delete the source branch.
refUpdates = append(refUpdates, git.RefUpdate{
Name: refSourceBranch,
Old: sourceBranchSHA,
New: sha.Nil,
})
}
now := time.Now() now := time.Now()
mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{ mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{
WriteParams: targetWriteParams, WriteParams: targetWriteParams,
@ -436,8 +489,7 @@ func (c *Controller) Merge(
CommitterDate: &now, CommitterDate: &now,
Author: author, Author: author,
AuthorDate: &now, AuthorDate: &now,
RefType: gitenum.RefTypeBranch, Refs: refUpdates,
RefName: pr.TargetBranch,
HeadExpectedSHA: sha.Must(in.SourceSHA), HeadExpectedSHA: sha.Must(in.SourceSHA),
Method: gitenum.MergeMethod(in.Method), Method: gitenum.MergeMethod(in.Method),
}) })
@ -545,27 +597,13 @@ func (c *Controller) Merge(
SourceSHA: mergeOutput.HeadSHA.String(), SourceSHA: mergeOutput.HeadSHA.String(),
}) })
var branchDeleted bool
if ruleOut.DeleteSourceBranch { if ruleOut.DeleteSourceBranch {
errDelete := c.git.DeleteBranch(ctx, &git.DeleteBranchParams{ pr.ActivitySeq = activitySeqBranchDeleted
WriteParams: sourceWriteParams, if _, errAct := c.activityStore.CreateWithPayload(ctx, pr, mergedBy,
BranchName: pr.SourceBranch, &types.PullRequestActivityPayloadBranchDelete{SHA: in.SourceSHA}, nil); errAct != nil {
})
if errDelete != nil {
// non-critical error // non-critical error
log.Ctx(ctx).Err(errDelete).Msgf("failed to delete source branch after merging") log.Ctx(ctx).Err(errAct).
} else { Msgf("failed to write pull request activity for successful automatic branch delete")
branchDeleted = true
// NOTE: there is a chance someone pushed on the branch between merge and delete.
// Either way, we'll use the SHA that was merged with for the activity to be consistent from PR perspective.
pr.ActivitySeq = activitySeqBranchDeleted
if _, errAct := c.activityStore.CreateWithPayload(ctx, pr, mergedBy,
&types.PullRequestActivityPayloadBranchDelete{SHA: in.SourceSHA}, nil); errAct != nil {
// non-critical error
log.Ctx(ctx).Err(errAct).
Msgf("failed to write pull request activity for successful automatic branch delete")
}
} }
} }
@ -621,7 +659,7 @@ func (c *Controller) Merge(
} }
return &types.MergeResponse{ return &types.MergeResponse{
SHA: mergeOutput.MergeSHA.String(), SHA: mergeOutput.MergeSHA.String(),
BranchDeleted: branchDeleted, BranchDeleted: ruleOut.DeleteSourceBranch,
RuleViolations: violations, RuleViolations: violations,
}, nil, nil }, nil, nil
} }

View File

@ -189,7 +189,7 @@ func (c *Controller) Create(
Name: strconv.FormatInt(targetRepo.PullReqSeq, 10), Name: strconv.FormatInt(targetRepo.PullReqSeq, 10),
Type: gitenum.RefTypePullReqHead, Type: gitenum.RefTypePullReqHead,
NewValue: sourceSHA, NewValue: sourceSHA,
OldValue: sha.None, // this is a new pull request, so we expect that the ref doesn't exist OldValue: sha.None, // we don't care about the old value
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to create PR head ref: %w", err) return fmt.Errorf("failed to create PR head ref: %w", err)

View File

@ -168,11 +168,21 @@ func (c *Controller) State(ctx context.Context,
pr.Closed = &nowMilli pr.Closed = &nowMilli
pr.MarkAsMergeUnchecked() pr.MarkAsMergeUnchecked()
// delete the merge pull request reference
err = c.git.UpdateRef(ctx, git.UpdateRefParams{
WriteParams: targetWriteParams,
Name: strconv.FormatInt(pr.Number, 10),
Type: gitenum.RefTypePullReqMerge,
NewValue: sha.Nil,
OldValue: sha.None, // we don't care about the old value
})
case changeReopen: case changeReopen:
pr.SourceSHA = sourceSHA.String() pr.SourceSHA = sourceSHA.String()
pr.MergeBaseSHA = mergeBaseSHA.String() pr.MergeBaseSHA = mergeBaseSHA.String()
pr.Closed = nil pr.Closed = nil
// create the head pull request reference
err = c.git.UpdateRef(ctx, git.UpdateRefParams{ err = c.git.UpdateRef(ctx, git.UpdateRefParams{
WriteParams: targetWriteParams, WriteParams: targetWriteParams,
Name: strconv.FormatInt(pr.Number, 10), Name: strconv.FormatInt(pr.Number, 10),

View File

@ -157,11 +157,18 @@ func (c *Controller) Rebase(
return nil, nil, fmt.Errorf("failed to create RPC write params: %w", err) return nil, nil, fmt.Errorf("failed to create RPC write params: %w", err)
} }
refType := gitenum.RefTypeBranch var refs []git.RefUpdate
refName := in.HeadBranch if !in.DryRun {
if in.DryRun { headBranchRef, err := git.GetRefPath(in.HeadBranch, gitenum.RefTypeBranch)
refType = gitenum.RefTypeUndefined if err != nil {
refName = "" return nil, nil, fmt.Errorf("failed to gerenere ref name: %w", err)
}
refs = append(refs, git.RefUpdate{
Name: headBranchRef,
Old: headBranch.Branch.SHA,
New: sha.SHA{}, // update to the result of the merge
})
} }
mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{ mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{
@ -169,8 +176,7 @@ func (c *Controller) Rebase(
BaseSHA: baseCommitSHA, BaseSHA: baseCommitSHA,
HeadRepoUID: repo.GitUID, HeadRepoUID: repo.GitUID,
HeadBranch: in.HeadBranch, HeadBranch: in.HeadBranch,
RefType: refType, Refs: refs,
RefName: refName,
HeadExpectedSHA: in.HeadCommitSHA, HeadExpectedSHA: in.HeadCommitSHA,
Method: gitenum.MergeMethodRebase, Method: gitenum.MergeMethodRebase,
}) })

View File

@ -135,6 +135,11 @@ func (c *Controller) Squash(
in.HeadCommitSHA, headBranch.Branch.Name) in.HeadCommitSHA, headBranch.Branch.Name)
} }
headBranchRef, err := git.GetRefPath(in.HeadBranch, gitenum.RefTypeBranch)
if err != nil {
return nil, nil, fmt.Errorf("failed to gerenere ref name: %w", err)
}
baseCommitSHA := in.BaseCommitSHA baseCommitSHA := in.BaseCommitSHA
if baseCommitSHA.IsEmpty() { if baseCommitSHA.IsEmpty() {
baseBranch, err := c.git.GetBranch(ctx, &git.GetBranchParams{ baseBranch, err := c.git.GetBranch(ctx, &git.GetBranchParams{
@ -153,11 +158,13 @@ func (c *Controller) Squash(
return nil, nil, fmt.Errorf("failed to create RPC write params: %w", err) return nil, nil, fmt.Errorf("failed to create RPC write params: %w", err)
} }
refType := gitenum.RefTypeBranch var refs []git.RefUpdate
refName := in.HeadBranch if !in.DryRun {
if in.DryRun { refs = append(refs, git.RefUpdate{
refType = gitenum.RefTypeUndefined Name: headBranchRef,
refName = "" Old: headBranch.Branch.SHA,
New: sha.SHA{}, // update to the result of the merge
})
} }
mergeBase, err := c.git.MergeBase(ctx, git.MergeBaseParams{ mergeBase, err := c.git.MergeBase(ctx, git.MergeBaseParams{
@ -223,8 +230,7 @@ func (c *Controller) Squash(
HeadRepoUID: repo.GitUID, HeadRepoUID: repo.GitUID,
HeadBranch: in.HeadBranch, HeadBranch: in.HeadBranch,
Message: git.CommitMessage(in.Title, in.Message), Message: git.CommitMessage(in.Title, in.Message),
RefType: refType, Refs: refs,
RefName: refName,
Committer: committer, Committer: committer,
CommitterDate: &now, CommitterDate: &now,
Author: author, Author: author,

View File

@ -79,46 +79,6 @@ func (s *Service) mergeCheckOnReopen(ctx context.Context,
) )
} }
// mergeCheckOnClosed deletes the merge ref.
func (s *Service) mergeCheckOnClosed(ctx context.Context,
event *events.Event[*pullreqevents.ClosedPayload],
) error {
return s.deleteMergeRef(ctx, event.Payload.SourceRepoID, event.Payload.Number)
}
// mergeCheckOnMerged deletes the merge ref.
func (s *Service) mergeCheckOnMerged(ctx context.Context,
event *events.Event[*pullreqevents.MergedPayload],
) error {
return s.deleteMergeRef(ctx, event.Payload.SourceRepoID, event.Payload.Number)
}
func (s *Service) deleteMergeRef(ctx context.Context, repoID int64, prNum int64) error {
repo, err := s.repoGitInfoCache.Get(ctx, repoID)
if err != nil {
return fmt.Errorf("failed to get repo with ID %d: %w", repoID, err)
}
writeParams, err := createSystemRPCWriteParams(ctx, s.urlProvider, repo.ID, repo.GitUID)
if err != nil {
return fmt.Errorf("failed to generate rpc write params: %w", err)
}
// TODO: This doesn't work for forked repos
err = s.git.UpdateRef(ctx, git.UpdateRefParams{
WriteParams: writeParams,
Name: strconv.Itoa(int(prNum)),
Type: gitenum.RefTypePullReqMerge,
NewValue: sha.None, // when NewValue is empty will delete the ref.
OldValue: sha.None, // we don't care about the old value
})
if err != nil {
return fmt.Errorf("failed to remove PR merge ref: %w", err)
}
return nil
}
//nolint:funlen // refactor if required. //nolint:funlen // refactor if required.
func (s *Service) updateMergeData( func (s *Service) updateMergeData(
ctx context.Context, ctx context.Context,
@ -186,6 +146,19 @@ func (s *Service) updateMergeData(
return fmt.Errorf("failed to generate rpc write params: %w", err) return fmt.Errorf("failed to generate rpc write params: %w", err)
} }
refName, err := git.GetRefPath(strconv.Itoa(int(pr.Number)), gitenum.RefTypePullReqMerge)
if err != nil {
return fmt.Errorf("failed to generate pull request merge ref name: %w", err)
}
refs := []git.RefUpdate{
{
Name: refName,
Old: sha.SHA{}, // no matter what the value of the reference is
New: sha.SHA{}, // update it to point to result of the merge
},
}
// call merge and store output in pr merge reference. // call merge and store output in pr merge reference.
now := time.Now() now := time.Now()
mergeOutput, err := s.git.Merge(ctx, &git.MergeParams{ mergeOutput, err := s.git.Merge(ctx, &git.MergeParams{
@ -193,8 +166,7 @@ func (s *Service) updateMergeData(
BaseBranch: pr.TargetBranch, BaseBranch: pr.TargetBranch,
HeadRepoUID: sourceRepo.GitUID, HeadRepoUID: sourceRepo.GitUID,
HeadBranch: pr.SourceBranch, HeadBranch: pr.SourceBranch,
RefType: gitenum.RefTypePullReqMerge, Refs: refs,
RefName: strconv.Itoa(int(pr.Number)),
HeadExpectedSHA: sha.Must(newSHA), HeadExpectedSHA: sha.Must(newSHA),
Force: true, Force: true,

View File

@ -194,8 +194,6 @@ func New(ctx context.Context,
_ = r.RegisterCreated(service.mergeCheckOnCreated) _ = r.RegisterCreated(service.mergeCheckOnCreated)
_ = r.RegisterBranchUpdated(service.mergeCheckOnBranchUpdate) _ = r.RegisterBranchUpdated(service.mergeCheckOnBranchUpdate)
_ = r.RegisterReopened(service.mergeCheckOnReopen) _ = r.RegisterReopened(service.mergeCheckOnReopen)
_ = r.RegisterClosed(service.mergeCheckOnClosed)
_ = r.RegisterMerged(service.mergeCheckOnMerged)
return nil return nil
}) })

View File

@ -104,14 +104,14 @@ func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams)
return nil, fmt.Errorf("failed to get target commit: %w", err) return nil, fmt.Errorf("failed to get target commit: %w", err)
} }
branchRef := api.GetReferenceFromBranchName(params.BranchName) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create ref updater to create the branch: %w", err) return nil, fmt.Errorf("failed to create ref updater to create the branch: %w", err)
} }
err = refUpdater.Do(ctx, sha.Nil, targetCommit.SHA) branchRef := api.GetReferenceFromBranchName(params.BranchName)
err = refUpdater.DoOne(ctx, branchRef, sha.Nil, targetCommit.SHA)
if errors.IsConflict(err) { if errors.IsConflict(err) {
return nil, errors.Conflict("branch %q already exists", params.BranchName) return nil, errors.Conflict("branch %q already exists", params.BranchName)
} }
@ -162,15 +162,16 @@ func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams)
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
branchRef := api.GetReferenceFromBranchName(params.BranchName)
commitSha, _ := sha.NewOrEmpty(params.SHA) commitSha, _ := sha.NewOrEmpty(params.SHA)
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create ref updater to create the branch: %w", err) return fmt.Errorf("failed to create ref updater to create the branch: %w", err)
} }
err = refUpdater.Do(ctx, commitSha, sha.Nil) branchRef := api.GetReferenceFromBranchName(params.BranchName)
err = refUpdater.DoOne(ctx, branchRef, commitSha, sha.Nil)
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
return errors.NotFound("branch %q does not exist", params.BranchName) return errors.NotFound("branch %q does not exist", params.BranchName)
} }

View File

@ -17,8 +17,7 @@ package enum
type RefType int type RefType int
const ( const (
RefTypeUndefined RefType = iota RefTypeRaw RefType = iota
RefTypeRaw
RefTypeBranch RefTypeBranch
RefTypeTag RefTypeTag
RefTypePullReqHead RefTypePullReqHead
@ -37,8 +36,6 @@ func (t RefType) String() string {
return "head" return "head"
case RefTypePullReqMerge: case RefTypePullReqMerge:
return "merge" return "merge"
case RefTypeUndefined:
fallthrough
default: default:
return "" return ""
} }

View File

@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
@ -32,7 +33,6 @@ func CreateRefUpdater(
hookClientFactory ClientFactory, hookClientFactory ClientFactory,
envVars map[string]string, envVars map[string]string,
repoPath string, repoPath string,
ref string,
) (*RefUpdater, error) { ) (*RefUpdater, error) {
if repoPath == "" { if repoPath == "" {
return nil, errors.Internal(nil, "repo path can't be empty") return nil, errors.Internal(nil, "repo path can't be empty")
@ -44,13 +44,11 @@ func CreateRefUpdater(
} }
return &RefUpdater{ return &RefUpdater{
state: stateInitOld, state: stateInit,
hookClient: client, hookClient: client,
envVars: envVars, envVars: envVars,
repoPath: repoPath, repoPath: repoPath,
ref: ref, refs: nil,
oldValue: sha.None,
newValue: sha.None,
}, nil }, nil
} }
@ -62,9 +60,7 @@ type RefUpdater struct {
hookClient Client hookClient Client
envVars map[string]string envVars map[string]string
repoPath string repoPath string
ref string refs []ReferenceUpdate
oldValue sha.SHA
newValue sha.SHA
} }
// refUpdaterState represents state of the ref updater internal state machine. // refUpdaterState represents state of the ref updater internal state machine.
@ -72,10 +68,8 @@ type refUpdaterState byte
func (t refUpdaterState) String() string { func (t refUpdaterState) String() string {
switch t { switch t {
case stateInitOld: case stateInit:
return "INIT_OLD" return "INIT"
case stateInitNew:
return "INIT_NEW"
case statePre: case statePre:
return "PRE" return "PRE"
case stateUpdate: case stateUpdate:
@ -89,8 +83,7 @@ func (t refUpdaterState) String() string {
} }
const ( const (
stateInitOld refUpdaterState = iota stateInit refUpdaterState = iota
stateInitNew
statePre statePre
stateUpdate stateUpdate
statePost statePost
@ -98,8 +91,8 @@ const (
) )
// Do runs full ref update by executing all methods in the correct order. // 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 { func (u *RefUpdater) Do(ctx context.Context, refs []ReferenceUpdate) error {
if err := u.Init(ctx, oldValue, newValue); err != nil { if err := u.Init(ctx, refs); err != nil {
return fmt.Errorf("init failed: %w", err) return fmt.Errorf("init failed: %w", err)
} }
@ -118,62 +111,70 @@ func (u *RefUpdater) Do(ctx context.Context, oldValue, newValue sha.SHA) error {
return nil return nil
} }
func (u *RefUpdater) Init(ctx context.Context, oldValue, newValue sha.SHA) error { // DoOne runs full ref update of only one reference.
if err := u.InitOld(ctx, oldValue); err != nil { func (u *RefUpdater) DoOne(ctx context.Context, ref string, oldValue, newValue sha.SHA) error {
return fmt.Errorf("init old failed: %w", err) return u.Do(ctx, []ReferenceUpdate{
} {
if err := u.InitNew(ctx, newValue); err != nil { Ref: ref,
return fmt.Errorf("init new failed: %w", err) Old: oldValue,
} New: newValue,
},
return nil })
} }
func (u *RefUpdater) InitOld(ctx context.Context, oldValue sha.SHA) error { func (u *RefUpdater) Init(ctx context.Context, refs []ReferenceUpdate) error {
if u == nil { if u.state != stateInit {
return nil
}
if u.state != stateInitOld {
return fmt.Errorf("invalid operation order: init old requires state=%s, current state=%s", return fmt.Errorf("invalid operation order: init old requires state=%s, current state=%s",
stateInitOld, u.state) stateInit, u.state)
} }
if oldValue.IsEmpty() { u.refs = make([]ReferenceUpdate, 0, len(refs))
// if no old value was provided, use current value (as required for hooks) for _, ref := range refs {
val, err := u.getRef(ctx) oldValue := ref.Old
if errors.IsNotFound(err) { //nolint:gocritic newValue := ref.New
oldValue = sha.Nil
} else if err != nil { var oldValueKnown bool
return fmt.Errorf("failed to get current value of reference: %w", err)
} else { if oldValue.IsEmpty() {
oldValue = val // if no old value was provided, use current value (as required for hooks)
val, err := u.getRef(ctx, ref.Ref)
if errors.IsNotFound(err) { //nolint:gocritic
oldValue = sha.Nil
} else if err != nil {
return fmt.Errorf("failed to get current value of reference %q: %w", ref.Ref, err)
} else {
oldValue = val
}
oldValueKnown = true
} }
if newValue.IsEmpty() {
// don't break existing interface - user calls with empty value to delete the ref.
newValue = sha.Nil
}
if oldValueKnown && oldValue == newValue {
// skip the unchanged refs
continue
}
u.refs = append(u.refs, ReferenceUpdate{
Ref: ref.Ref,
Old: oldValue,
New: newValue,
})
} }
u.state = stateInitNew if len(refs) > 0 && len(u.refs) == 0 {
u.oldValue = oldValue return errors.New("updating zero references")
return nil
}
func (u *RefUpdater) InitNew(_ context.Context, newValue sha.SHA) error {
if u == nil {
return nil
} }
if u.state != stateInitNew { sort.Slice(u.refs, func(i, j int) bool {
return fmt.Errorf("invalid operation order: init new requires state=%s, current state=%s", return u.refs[i].Ref < u.refs[j].Ref
stateInitNew, u.state) })
}
if newValue.IsEmpty() {
// don't break existing interface - user calls with empty value to delete the ref.
newValue = sha.Nil
}
u.state = statePre u.state = statePre
u.newValue = newValue
return nil return nil
} }
@ -185,23 +186,13 @@ func (u *RefUpdater) Pre(ctx context.Context, alternateDirs ...string) error {
statePre, u.state) statePre, u.state)
} }
// fail in case someone tries to delete a reference that doesn't exist. if len(u.refs) == 0 {
if u.oldValue.IsEmpty() && u.newValue.IsNil() { u.state = stateUpdate
return errors.NotFound("reference %q not found", u.ref) return nil
}
if u.oldValue.IsNil() && u.newValue.IsNil() {
return fmt.Errorf("provided values cannot be both empty")
} }
out, err := u.hookClient.PreReceive(ctx, PreReceiveInput{ out, err := u.hookClient.PreReceive(ctx, PreReceiveInput{
RefUpdates: []ReferenceUpdate{ RefUpdates: u.refs,
{
Ref: u.ref,
Old: u.oldValue,
New: u.newValue,
},
},
Environment: Environment{ Environment: Environment{
AlternateObjectDirs: alternateDirs, AlternateObjectDirs: alternateDirs,
}, },
@ -228,22 +219,33 @@ func (u *RefUpdater) UpdateRef(ctx context.Context) error {
stateUpdate, u.state) stateUpdate, u.state)
} }
cmd := command.New("update-ref") if len(u.refs) == 0 {
if u.newValue.IsNil() { u.state = statePost
cmd.Add(command.WithFlag("-d", u.ref)) return nil
} else {
cmd.Add(command.WithArg(u.ref, u.newValue.String()))
} }
cmd.Add(command.WithArg(u.oldValue.String())) input := bytes.NewBuffer(nil)
for _, ref := range u.refs {
switch {
case ref.New.IsNil():
_, _ = input.WriteString(fmt.Sprintf("delete %s\000%s\000", ref.Ref, ref.Old))
case ref.Old.IsNil():
_, _ = input.WriteString(fmt.Sprintf("create %s\000%s\000", ref.Ref, ref.New))
default:
_, _ = input.WriteString(fmt.Sprintf("update %s\000%s\000%s\000", ref.Ref, ref.New, ref.Old))
}
}
if err := cmd.Run(ctx, command.WithDir(u.repoPath)); err != nil { input.WriteString("commit\000")
cmd := command.New("update-ref", command.WithFlag("--stdin"), command.WithFlag("-z"))
if err := cmd.Run(ctx, command.WithStdin(input), command.WithDir(u.repoPath)); err != nil {
msg := err.Error() msg := err.Error()
if strings.Contains(msg, "reference already exists") { if strings.Contains(msg, "reference already exists") {
return errors.Conflict("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) return fmt.Errorf("update of references %v failed: %w", u.refs, err)
} }
u.state = statePost u.state = statePost
@ -258,14 +260,13 @@ func (u *RefUpdater) Post(ctx context.Context, alternateDirs ...string) error {
statePost, u.state) statePost, u.state)
} }
if len(u.refs) == 0 {
u.state = stateDone
return nil
}
out, err := u.hookClient.PostReceive(ctx, PostReceiveInput{ out, err := u.hookClient.PostReceive(ctx, PostReceiveInput{
RefUpdates: []ReferenceUpdate{ RefUpdates: u.refs,
{
Ref: u.ref,
Old: u.oldValue,
New: u.newValue,
},
},
Environment: Environment{ Environment: Environment{
AlternateObjectDirs: alternateDirs, AlternateObjectDirs: alternateDirs,
}, },
@ -282,16 +283,16 @@ func (u *RefUpdater) Post(ctx context.Context, alternateDirs ...string) error {
return nil return nil
} }
func (u *RefUpdater) getRef(ctx context.Context) (sha.SHA, error) { func (u *RefUpdater) getRef(ctx context.Context, ref string) (sha.SHA, error) {
cmd := command.New("show-ref", cmd := command.New("show-ref",
command.WithFlag("--verify"), command.WithFlag("--verify"),
command.WithFlag("-s"), command.WithFlag("-s"),
command.WithArg(u.ref), command.WithArg(ref),
) )
output := &bytes.Buffer{} output := &bytes.Buffer{}
err := cmd.Run(ctx, command.WithDir(u.repoPath), command.WithStdout(output)) err := cmd.Run(ctx, command.WithDir(u.repoPath), command.WithStdout(output))
if cErr := command.AsError(err); cErr != nil && cErr.IsExitCode(128) && cErr.IsInvalidRefErr() { if cErr := command.AsError(err); cErr != nil && cErr.IsExitCode(128) && cErr.IsInvalidRefErr() {
return sha.None, errors.NotFound("reference %q not found", u.ref) return sha.None, errors.NotFound("reference %q not found", ref)
} }
if err != nil { if err != nil {

View File

@ -26,6 +26,7 @@ import (
"github.com/harness/gitness/git/merge" "github.com/harness/gitness/git/merge"
"github.com/harness/gitness/git/parser" "github.com/harness/gitness/git/parser"
"github.com/harness/gitness/git/sha" "github.com/harness/gitness/git/sha"
"github.com/harness/gitness/git/sharedrepo"
) )
// MergeParams is input structure object for merging operation. // MergeParams is input structure object for merging operation.
@ -56,8 +57,7 @@ type MergeParams struct {
// (optional, default: committer date) // (optional, default: committer date)
AuthorDate *time.Time AuthorDate *time.Time
RefType enum.RefType Refs []RefUpdate
RefName string
// HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older // HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older
// than the HeadBranch latest sha then merge will fail. // than the HeadBranch latest sha then merge will fail.
@ -69,6 +69,19 @@ type MergeParams struct {
Method enum.MergeMethod Method enum.MergeMethod
} }
type RefUpdate struct {
// Name is the full name of the reference.
Name string
// Old is the expected current value of the reference.
// If it's empty, the old value of the reference can be any value.
Old sha.SHA
// New is the desired value for the reference.
// If it's empty, the reference would be set to the resulting commit SHA of the merge.
New sha.SHA
}
func (p *MergeParams) Validate() error { func (p *MergeParams) Validate() error {
if err := p.WriteParams.Validate(); err != nil { if err := p.WriteParams.Validate(); err != nil {
return err return err
@ -82,9 +95,12 @@ func (p *MergeParams) Validate() error {
return errors.InvalidArgument("head branch is mandatory") return errors.InvalidArgument("head branch is mandatory")
} }
if p.RefType != enum.RefTypeUndefined && p.RefName == "" { for _, ref := range p.Refs {
return errors.InvalidArgument("ref name has to be provided if type is defined") if ref.Name == "" {
return errors.InvalidArgument("ref name has to be provided")
}
} }
return nil return nil
} }
@ -153,27 +169,6 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
panic("unsupported merge method") panic("unsupported merge method")
} }
// set up the target reference
var refPath string
var refOldValue sha.SHA
if params.RefType != enum.RefTypeUndefined {
refPath, err = GetRefPath(params.RefName, params.RefType)
if err != nil {
return MergeOutput{}, fmt.Errorf(
"failed to generate full reference for type '%s' and name '%s' for merge operation: %w",
params.RefType, params.RefName, err)
}
refOldValue, err = s.git.GetFullCommitID(ctx, repoPath, refPath)
if errors.IsNotFound(err) {
refOldValue = sha.Nil
} else if err != nil {
return MergeOutput{}, fmt.Errorf("failed to resolve %q: %w", refPath, err)
}
}
// find the commit SHAs // find the commit SHAs
baseCommitSHA := params.BaseSHA baseCommitSHA := params.BaseSHA
@ -259,35 +254,66 @@ func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput,
message = parser.CleanUpWhitespace(params.Message) message = parser.CleanUpWhitespace(params.Message)
} }
// merge // create merge commit and update the references
var refUpdater *hook.RefUpdater refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil {
if params.RefType != enum.RefTypeUndefined { return MergeOutput{}, fmt.Errorf("failed to create reference updater: %w", err)
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( var mergeCommitSHA sha.SHA
ctx, var conflicts []string
refUpdater,
repoPath, s.tmpDir, err = sharedrepo.Run(ctx, refUpdater, s.tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error {
&author, &committer, mergeCommitSHA, conflicts, err = mergeFunc(
message, ctx,
mergeBaseCommitSHA, baseCommitSHA, headCommitSHA) s,
merge.Params{
Author: &author,
Committer: &committer,
Message: message,
MergeBaseSHA: mergeBaseCommitSHA,
TargetSHA: baseCommitSHA,
SourceSHA: headCommitSHA,
})
if err != nil {
return fmt.Errorf("failed to create merge commit: %w", err)
}
if mergeCommitSHA.IsEmpty() || len(conflicts) > 0 {
return refUpdater.Init(ctx, nil) // update nothing
}
refUpdates := make([]hook.ReferenceUpdate, len(params.Refs))
for i, ref := range params.Refs {
oldValue := ref.Old
newValue := ref.New
if newValue.IsEmpty() { // replace all empty new values to the result of the merge
newValue = mergeCommitSHA
}
refUpdates[i] = hook.ReferenceUpdate{
Ref: ref.Name,
Old: oldValue,
New: newValue,
}
}
err = refUpdater.Init(ctx, refUpdates)
if err != nil {
return fmt.Errorf("failed to init values of references (%v): %w", refUpdates, err)
}
return nil
})
if errors.IsConflict(err) { if errors.IsConflict(err) {
return MergeOutput{}, fmt.Errorf("failed to merge %q to %q in %q using the %q merge method: %w", return MergeOutput{}, fmt.Errorf("failed to merge %q to %q in %q using the %q merge method: %w",
params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod, err) params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod, err)
} }
if err != nil { if err != nil {
return MergeOutput{}, errors.Internal(err, "failed to merge %q to %q in %q using the %q merge method", return MergeOutput{}, fmt.Errorf("failed to merge %q to %q in %q using the %q merge method: %w",
params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod) params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod, err)
} }
if len(conflicts) > 0 { if len(conflicts) > 0 {
return MergeOutput{ return MergeOutput{

View File

@ -20,222 +20,175 @@ import (
"github.com/harness/gitness/errors" "github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api" "github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/sha" "github.com/harness/gitness/git/sha"
"github.com/harness/gitness/git/sharedrepo" "github.com/harness/gitness/git/sharedrepo"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
var ( type Params struct {
// errConflict is used to error out of sharedrepo Run method without erroring out of merge in case of conflicts. Author, Committer *api.Signature
errConflict = errors.New("conflict") Message string
) MergeBaseSHA, TargetSHA, SourceSHA sha.SHA
}
// Func represents a merge method function. The concrete merge implementation functions must have this signature. // Func represents a merge method function. The concrete merge implementation functions must have this signature.
type Func func( type Func func(
ctx context.Context, ctx context.Context,
refUpdater *hook.RefUpdater, s *sharedrepo.SharedRepo,
repoPath, tmpDir string, params Params,
author, committer *api.Signature,
message string,
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA sha.SHA, conflicts []string, err error) ) (mergeSHA sha.SHA, conflicts []string, err error)
// Merge merges two the commits (targetSHA and sourceSHA) using the Merge method. // Merge merges two the commits (targetSHA and sourceSHA) using the Merge method.
func Merge( func Merge(
ctx context.Context, ctx context.Context,
refUpdater *hook.RefUpdater, s *sharedrepo.SharedRepo,
repoPath, tmpDir string, params Params,
author, committer *api.Signature,
message string,
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA sha.SHA, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
return mergeInternal(ctx, return mergeInternal(ctx, s, params, false)
refUpdater,
repoPath, tmpDir,
author, committer,
message,
mergeBaseSHA, targetSHA, sourceSHA,
false)
} }
// Squash merges two the commits (targetSHA and sourceSHA) using the Squash method. // Squash merges two the commits (targetSHA and sourceSHA) using the Squash method.
func Squash( func Squash(
ctx context.Context, ctx context.Context,
refUpdater *hook.RefUpdater, s *sharedrepo.SharedRepo,
repoPath, tmpDir string, params Params,
author, committer *api.Signature,
message string,
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA sha.SHA, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
return mergeInternal(ctx, return mergeInternal(ctx, s, params, true)
refUpdater,
repoPath, tmpDir,
author, committer,
message,
mergeBaseSHA, targetSHA, sourceSHA,
true)
} }
// mergeInternal is internal implementation of merge used for Merge and Squash methods. // mergeInternal is internal implementation of merge used for Merge and Squash methods.
func mergeInternal( func mergeInternal(ctx context.Context,
ctx context.Context, s *sharedrepo.SharedRepo,
refUpdater *hook.RefUpdater, params Params,
repoPath, tmpDir string,
author, committer *api.Signature,
message string,
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
squash bool, squash bool,
) (mergeSHA sha.SHA, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
err = sharedrepo.Run(ctx, refUpdater, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error { mergeBaseSHA := params.MergeBaseSHA
var err error targetSHA := params.TargetSHA
sourceSHA := params.SourceSHA
var treeSHA sha.SHA var treeSHA sha.SHA
treeSHA, conflicts, err = s.MergeTree(ctx, mergeBaseSHA, targetSHA, sourceSHA) treeSHA, conflicts, err = s.MergeTree(ctx, mergeBaseSHA, targetSHA, sourceSHA)
if err != nil { if err != nil {
return fmt.Errorf("merge tree failed: %w", err) return sha.None, nil, fmt.Errorf("merge tree failed: %w", err)
} }
if len(conflicts) > 0 { if len(conflicts) > 0 {
return errConflict return sha.None, conflicts, nil
} }
parents := make([]sha.SHA, 0, 2) parents := make([]sha.SHA, 0, 2)
parents = append(parents, targetSHA) parents = append(parents, targetSHA)
if !squash { if !squash {
parents = append(parents, sourceSHA) parents = append(parents, sourceSHA)
} }
mergeSHA, err = s.CommitTree(ctx, author, committer, treeSHA, message, false, parents...) mergeSHA, err = s.CommitTree(ctx, params.Author, params.Committer, treeSHA, params.Message, false, parents...)
if err != nil { if err != nil {
return fmt.Errorf("commit tree failed: %w", err) return sha.None, nil, 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 && !errors.Is(err, errConflict) {
return sha.None, nil, fmt.Errorf("merge method=merge squash=%t: %w", squash, err)
} }
return mergeSHA, conflicts, nil return mergeSHA, conflicts, nil
} }
// Rebase merges two the commits (targetSHA and sourceSHA) using the Rebase method. // Rebase merges two the commits (targetSHA and sourceSHA) using the Rebase method.
// Commit author isn't used here - it's copied from every commit.
// Commit message isn't used here
// //
//nolint:gocognit // refactor if needed. //nolint:gocognit // refactor if needed.
func Rebase( func Rebase(
ctx context.Context, ctx context.Context,
refUpdater *hook.RefUpdater, s *sharedrepo.SharedRepo,
repoPath, tmpDir string, params Params,
_, 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) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
err = sharedrepo.Run(ctx, refUpdater, tmpDir, repoPath, func(s *sharedrepo.SharedRepo) error { mergeBaseSHA := params.MergeBaseSHA
sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA) targetSHA := params.TargetSHA
if err != nil { sourceSHA := params.SourceSHA
return fmt.Errorf("failed to find commit list in rebase merge: %w", err)
}
lastCommitSHA := targetSHA sourceSHAs, err := s.CommitSHAsForRebase(ctx, mergeBaseSHA, sourceSHA)
lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA.String()) if err != nil {
if err != nil { return sha.None, nil, fmt.Errorf("failed to find commit list in rebase merge: %w", err)
return fmt.Errorf("failed to get tree sha for target: %w", err)
}
for _, commitSHA := range sourceSHAs {
var treeSHA sha.SHA
commitInfo, err := api.GetCommit(ctx, s.Directory(), commitSHA.String())
if err != nil {
return fmt.Errorf("failed to get commit data in rebase merge: %w", err)
}
// rebase merge preserves the commit author (and date) and the commit message, but changes the committer.
author := &commitInfo.Author
message := commitInfo.Title
if commitInfo.Message != "" {
message += "\n\n" + commitInfo.Message
}
var mergeTreeMergeBaseSHA sha.SHA
if len(commitInfo.ParentSHAs) > 0 {
// use parent of commit as merge base to only apply changes introduced by commit.
// See example usage of when --merge-base was introduced:
// https://github.com/git/git/commit/66265a693e8deb3ab86577eb7f69940410044081
//
// NOTE: CommitSHAsForRebase only returns non-merge commits.
mergeTreeMergeBaseSHA = commitInfo.ParentSHAs[0]
}
treeSHA, conflicts, err = s.MergeTree(ctx, mergeTreeMergeBaseSHA, lastCommitSHA, commitSHA)
if err != nil {
return fmt.Errorf("failed to merge tree in rebase merge: %w", err)
}
if len(conflicts) > 0 {
return errConflict
}
// Drop any commit which after being rebased would be empty.
// There's two cases in which that can happen:
// 1. Empty commit.
// Github is dropping empty commits, so we'll do the same.
// 2. The changes of the commit already exist on the target branch.
// Git's `git rebase` is dropping such commits on default (and so does Github)
// https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---emptydropkeepask
if treeSHA.Equal(lastTreeSHA) {
log.Ctx(ctx).Debug().Msgf("skipping commit %s as it's empty after rebase", commitSHA)
continue
}
lastCommitSHA, err = s.CommitTree(ctx, author, committer, treeSHA, message, false, lastCommitSHA)
if err != nil {
return fmt.Errorf("failed to commit tree in rebase merge: %w", err)
}
lastTreeSHA = treeSHA
}
if err := refUpdater.InitNew(ctx, lastCommitSHA); err != nil {
return fmt.Errorf("refUpdater.InitNew failed: %w", err)
}
mergeSHA = lastCommitSHA
return nil
})
if err != nil && !errors.Is(err, errConflict) {
return sha.None, nil, fmt.Errorf("merge method=rebase: %w", err)
} }
return mergeSHA, conflicts, nil lastCommitSHA := targetSHA
lastTreeSHA, err := s.GetTreeSHA(ctx, targetSHA.String())
if err != nil {
return sha.None, nil, fmt.Errorf("failed to get tree sha for target: %w", err)
}
for _, commitSHA := range sourceSHAs {
var treeSHA sha.SHA
commitInfo, err := api.GetCommit(ctx, s.Directory(), commitSHA.String())
if err != nil {
return sha.None, nil, fmt.Errorf("failed to get commit data in rebase merge: %w", err)
}
// rebase merge preserves the commit author (and date) and the commit message, but changes the committer.
author := &commitInfo.Author
message := commitInfo.Title
if commitInfo.Message != "" {
message += "\n\n" + commitInfo.Message
}
var mergeTreeMergeBaseSHA sha.SHA
if len(commitInfo.ParentSHAs) > 0 {
// use parent of commit as merge base to only apply changes introduced by commit.
// See example usage of when --merge-base was introduced:
// https://github.com/git/git/commit/66265a693e8deb3ab86577eb7f69940410044081
//
// NOTE: CommitSHAsForRebase only returns non-merge commits.
mergeTreeMergeBaseSHA = commitInfo.ParentSHAs[0]
}
treeSHA, conflicts, err = s.MergeTree(ctx, mergeTreeMergeBaseSHA, lastCommitSHA, commitSHA)
if err != nil {
return sha.None, nil, fmt.Errorf("failed to merge tree in rebase merge: %w", err)
}
if len(conflicts) > 0 {
return sha.None, conflicts, nil
}
// Drop any commit which after being rebased would be empty.
// There's two cases in which that can happen:
// 1. Empty commit.
// Github is dropping empty commits, so we'll do the same.
// 2. The changes of the commit already exist on the target branch.
// Git's `git rebase` is dropping such commits on default (and so does Github)
// https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---emptydropkeepask
if treeSHA.Equal(lastTreeSHA) {
log.Ctx(ctx).Debug().Msgf("skipping commit %s as it's empty after rebase", commitSHA)
continue
}
lastCommitSHA, err = s.CommitTree(ctx, author, params.Committer, treeSHA, message, false, lastCommitSHA)
if err != nil {
return sha.None, nil, fmt.Errorf("failed to commit tree in rebase merge: %w", err)
}
lastTreeSHA = treeSHA
}
mergeSHA = lastCommitSHA
return mergeSHA, nil, nil
} }
// FastForward points the is internal implementation of merge used for Merge and Squash methods. // FastForward points the is internal implementation of merge used for Merge and Squash methods.
// Commit author and committer aren't used here. Commit message isn't used here.
func FastForward( func FastForward(
ctx context.Context, _ context.Context,
refUpdater *hook.RefUpdater, _ *sharedrepo.SharedRepo,
repoPath, tmpDir string, params Params,
_, _ *api.Signature, // commit author and committer aren't used here
_ string, // commit message isn't used here
mergeBaseSHA, targetSHA, sourceSHA sha.SHA,
) (mergeSHA sha.SHA, conflicts []string, err error) { ) (mergeSHA sha.SHA, conflicts []string, err error) {
mergeBaseSHA := params.MergeBaseSHA
targetSHA := params.TargetSHA
sourceSHA := params.SourceSHA
if targetSHA != mergeBaseSHA { if targetSHA != mergeBaseSHA {
return sha.None, nil, return sha.None, nil,
errors.Conflict("Target branch has diverged from the source branch. Fast-forward not possible.") errors.Conflict("Target branch has diverged from the source branch. Fast-forward not possible.")
} }
err = sharedrepo.Run(ctx, refUpdater, tmpDir, repoPath, func(*sharedrepo.SharedRepo) error {
return refUpdater.InitNew(ctx, sourceSHA)
})
if err != nil {
return sha.None, nil, fmt.Errorf("merge method=fast-forward: %w", err)
}
return sourceSHA, nil, nil return sourceSHA, nil, nil
} }

View File

@ -144,7 +144,7 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
refOldSHA = commit.SHA refOldSHA = commit.SHA
} }
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil { if err != nil {
return CommitFilesResponse{}, fmt.Errorf("failed to create ref updater: %w", err) return CommitFilesResponse{}, fmt.Errorf("failed to create ref updater: %w", err)
} }
@ -222,7 +222,13 @@ func (s *Service) CommitFiles(ctx context.Context, params *CommitFilesParams) (C
refNewSHA = commitSHA refNewSHA = commitSHA
if err := refUpdater.Init(ctx, refOldSHA, refNewSHA); err != nil { ref := hook.ReferenceUpdate{
Ref: branchRef,
Old: refOldSHA,
New: refNewSHA,
}
if err := refUpdater.Init(ctx, []hook.ReferenceUpdate{ref}); err != nil {
return fmt.Errorf("failed to init ref updater old=%s new=%s: %w", refOldSHA, refNewSHA, err) return fmt.Errorf("failed to init ref updater old=%s new=%s: %w", refOldSHA, refNewSHA, err)
} }

View File

@ -89,15 +89,15 @@ func (s *Service) UpdateRef(ctx context.Context, params UpdateRefParams) error {
reference, err := GetRefPath(params.Name, params.Type) reference, err := GetRefPath(params.Name, params.Type)
if err != nil { if err != nil {
return fmt.Errorf("UpdateRef: failed to fetch reference '%s': %w", params.Name, err) return fmt.Errorf("failed to create reference '%s': %w", params.Name, err)
} }
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, reference) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil { if err != nil {
return fmt.Errorf("UpdateRef: failed to create ref updater: %w", err) return fmt.Errorf("failed to create ref updater: %w", err)
} }
if err := refUpdater.Do(ctx, params.OldValue, params.NewValue); err != nil { if err := refUpdater.DoOne(ctx, reference, params.OldValue, params.NewValue); err != nil {
return fmt.Errorf("failed to update ref: %w", err) return fmt.Errorf("failed to update ref: %w", err)
} }
@ -122,8 +122,6 @@ func GetRefPath(refName string, refType enum.RefType) (string, error) {
return refPullReqPrefix + refName + refPullReqHeadSuffix, nil return refPullReqPrefix + refName + refPullReqHeadSuffix, nil
case enum.RefTypePullReqMerge: case enum.RefTypePullReqMerge:
return refPullReqPrefix + refName + refPullReqMergeSuffix, nil return refPullReqPrefix + refName + refPullReqMergeSuffix, nil
case enum.RefTypeUndefined:
fallthrough
default: default:
return "", errors.InvalidArgument("provided reference type '%s' is invalid", refType) return "", errors.InvalidArgument("provided reference type '%s' is invalid", refType)
} }

View File

@ -278,7 +278,7 @@ func (s *Service) CreateCommitTag(ctx context.Context, params *CreateCommitTagPa
// ref updater // ref updater
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, tagRef) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create ref updater to create the tag: %w", err) return nil, fmt.Errorf("failed to create ref updater to create the tag: %w", err)
} }
@ -295,7 +295,13 @@ func (s *Service) CreateCommitTag(ctx context.Context, params *CreateCommitTagPa
return fmt.Errorf("failed to read annotated tag after creation: %w", err) return fmt.Errorf("failed to read annotated tag after creation: %w", err)
} }
if err := refUpdater.Init(ctx, sha.Nil, tag.Sha); err != nil { ref := hook.ReferenceUpdate{
Ref: tagRef,
Old: sha.Nil,
New: tag.Sha,
}
if err := refUpdater.Init(ctx, []hook.ReferenceUpdate{ref}); err != nil {
return fmt.Errorf("failed to init ref updater: %w", err) return fmt.Errorf("failed to init ref updater: %w", err)
} }
@ -337,14 +343,15 @@ func (s *Service) DeleteTag(ctx context.Context, params *DeleteTagParams) error
} }
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID) repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
tagRef := api.GetReferenceFromTagName(params.Name)
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, tagRef) refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create ref updater to delete the tag: %w", err) 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 tagRef := api.GetReferenceFromTagName(params.Name)
err = refUpdater.DoOne(ctx, tagRef, sha.None, sha.Nil) // delete whatever is there
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
return errors.NotFound("tag %q does not exist", params.Name) return errors.NotFound("tag %q does not exist", params.Name)
} }