internal/gitaly/service/operations/merge_to_ref.go (121 lines of code) (raw):
package operations
import (
"context"
"errors"
"fmt"
"strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/git"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/log"
"gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
)
// UserMergeToRef overwrites the given TargetRef to point to either Branch or
// FirstParentRef. Afterwards, it performs a merge of SourceSHA with either
// Branch or FirstParentRef and updates TargetRef to the merge commit.
func (s *Server) UserMergeToRef(ctx context.Context, request *gitalypb.UserMergeToRefRequest) (*gitalypb.UserMergeToRefResponse, error) {
if err := validateUserMergeToRefRequest(ctx, s.locator, request); err != nil {
return nil, structerr.NewInvalidArgument("%w", err)
}
repo := s.localRepoFactory.Build(request.GetRepository())
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return nil, fmt.Errorf("detecting object hash: %w", err)
}
//nolint:staticcheck // Branch is marked as deprecated in the protobuf
revision := git.Revision(request.GetBranch())
if request.FirstParentRef != nil {
revision = git.Revision(request.GetFirstParentRef())
}
oid, err := repo.ResolveRevision(ctx, revision)
if err != nil {
return nil, structerr.NewInvalidArgument("Invalid merge source")
}
sourceOID, err := repo.ResolveRevision(ctx, git.Revision(request.GetSourceSha()))
if err != nil {
return nil, structerr.NewInvalidArgument("Invalid merge source")
}
authorSignature, err := git.SignatureFromRequest(request)
if err != nil {
return nil, structerr.NewInvalidArgument("%w", err)
}
// Initialize oldTargetOID from expected_old_oid when provided, otherwise
// resolve it from target_ref. This will be used as an optimistic lock when
// we finally update target_ref, to ensure it hasn't changed in the
// meantime.
var oldTargetOID git.ObjectID
if expectedOldOID := request.GetExpectedOldOid(); expectedOldOID != "" {
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return nil, structerr.NewInternal("detecting object hash: %w", err)
}
oldTargetOID, err = objectHash.FromHex(expectedOldOID)
if err != nil {
return nil, structerr.NewInvalidArgument("invalid expected old object ID: %w", err).WithMetadata("old_object_id", expectedOldOID)
}
oldTargetOID, err = resolveRevision(ctx, repo, oldTargetOID)
if err != nil {
return nil, structerr.NewInvalidArgument("cannot resolve expected old object ID: %w", err).
WithMetadata("old_object_id", expectedOldOID)
}
} else if targetRef, err := repo.GetReference(ctx, git.ReferenceName(request.GetTargetRef())); err == nil {
if targetRef.IsSymbolic {
return nil, structerr.NewFailedPrecondition("target reference is symbolic: %q", request.GetTargetRef())
}
oid, err := objectHash.FromHex(targetRef.Target)
if err != nil {
return nil, structerr.NewInternal("invalid target revision: %w", err)
}
oldTargetOID = oid
} else if errors.Is(err, git.ErrReferenceAmbiguous) {
return nil, structerr.NewInvalidArgument("target reference is ambiguous: %w", err)
} else if errors.Is(err, git.ErrReferenceNotFound) {
oldTargetOID = objectHash.ZeroOID
} else {
return nil, structerr.NewInternal("could not read target reference: %w", err)
}
mergeCommitID, err := s.merge(
ctx,
repo,
authorSignature,
authorSignature,
string(request.GetMessage()),
oid.String(),
sourceOID.String(),
false,
)
if err != nil {
s.logger.WithError(err).WithFields(
log.Fields{
"source_sha": sourceOID,
"target_sha": oid,
"target_ref": string(request.GetTargetRef()),
},
).ErrorContext(ctx, "unable to create merge commit")
return nil, structerr.NewFailedPrecondition("Failed to create merge commit for source_sha %s and target_sha %s at %s",
sourceOID, oid, string(request.GetTargetRef()))
}
mergeOID, err := objectHash.FromHex(mergeCommitID)
if err != nil {
return nil, structerr.NewInternal("parsing merge commit SHA: %w", err)
}
// ... and move branch from target ref to the merge commit. The Ruby
// implementation doesn't invoke hooks, so we don't either.
if err := repo.UpdateRef(ctx, git.ReferenceName(request.GetTargetRef()), mergeOID, oldTargetOID); err != nil {
return nil, structerr.NewFailedPrecondition("Could not update %s. Please refresh and try again", string(request.GetTargetRef()))
}
return &gitalypb.UserMergeToRefResponse{
CommitId: mergeOID.String(),
}, nil
}
func validateUserMergeToRefRequest(ctx context.Context, locator storage.Locator, in *gitalypb.UserMergeToRefRequest) error {
if err := locator.ValidateRepository(ctx, in.GetRepository()); err != nil {
return err
}
//nolint:staticcheck // Branch is marked as deprecated in the protobuf
if len(in.GetFirstParentRef()) == 0 && len(in.GetBranch()) == 0 {
return errors.New("empty first parent ref and branch name")
}
if in.GetUser() == nil {
return errors.New("empty user")
}
if in.GetSourceSha() == "" {
return errors.New("empty source SHA")
}
if len(in.GetTargetRef()) == 0 {
return errors.New("empty target ref")
}
if !strings.HasPrefix(string(in.GetTargetRef()), "refs/merge-requests") {
return errors.New("invalid target ref")
}
return nil
}