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 }