// Copyright 2022 Harness Inc. All rights reserved. // Use of this source code is governed by the Polyform Free Trial License // that can be found in the LICENSE.md file for this repository. package pullreq import ( "context" "fmt" "strconv" "time" "github.com/harness/gitness/events" "github.com/harness/gitness/gitrpc" gitrpcenum "github.com/harness/gitness/gitrpc/enum" pullreqevents "github.com/harness/gitness/internal/events/pullreq" "github.com/harness/gitness/pubsub" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) const ( cancelMergeCheckKey = "cancel_merge_check_for_sha" nilSHA = "0000000000000000000000000000000000000000" ) // mergeCheckOnCreated handles pull request Created events. // It creates the PR head git ref. func (s *Service) mergeCheckOnCreated(ctx context.Context, event *events.Event[*pullreqevents.CreatedPayload], ) error { return s.updateMergeData( ctx, event.Payload.TargetRepoID, event.Payload.Number, nilSHA, event.Payload.SourceSHA, ) } // mergeCheckOnBranchUpdate handles pull request Branch Updated events. // It updates the PR head git ref to point to the latest commit. func (s *Service) mergeCheckOnBranchUpdate(ctx context.Context, event *events.Event[*pullreqevents.BranchUpdatedPayload], ) error { return s.updateMergeData( ctx, event.Payload.TargetRepoID, event.Payload.Number, event.Payload.OldSHA, event.Payload.NewSHA, ) } // mergeCheckOnReopen handles pull request StateChanged events. // It updates the PR head git ref to point to the source branch commit SHA. func (s *Service) mergeCheckOnReopen(ctx context.Context, event *events.Event[*pullreqevents.ReopenedPayload], ) error { return s.updateMergeData( ctx, event.Payload.TargetRepoID, event.Payload.Number, "", event.Payload.SourceSHA, ) } // 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.gitRPCClient.UpdateRef(ctx, gitrpc.UpdateRefParams{ WriteParams: writeParams, Name: strconv.Itoa(int(prNum)), Type: gitrpcenum.RefTypePullReqMerge, NewValue: "", // when NewValue is empty gitrpc will delete the ref. OldValue: "", // 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. func (s *Service) updateMergeData( ctx context.Context, repoID int64, prNum int64, oldSHA string, newSHA string, ) error { // TODO: Merge check should not update the merge base. // TODO: Instead it should accept it as an argument and fail if it doesn't match. // Then is would not longer be necessary to cancel already active mergeability checks. pr, err := s.pullreqStore.FindByNumber(ctx, repoID, prNum) if err != nil { return fmt.Errorf("failed to get pull request number %d: %w", prNum, err) } if pr.State != enum.PullReqStateOpen { return fmt.Errorf("cannot do mergability check on closed PR %d", prNum) } // cancel all previous mergability work for this PR based on oldSHA if err = s.pubsub.Publish(ctx, cancelMergeCheckKey, []byte(oldSHA), pubsub.WithPublishNamespace("pullreq")); err != nil { return err } var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) s.cancelMutex.Lock() s.cancelMergeability[newSHA] = cancel s.cancelMutex.Unlock() defer func() { cancel() s.cancelMutex.Lock() delete(s.cancelMergeability, newSHA) s.cancelMutex.Unlock() }() // load repository objects targetRepo, err := s.repoGitInfoCache.Get(ctx, pr.TargetRepoID) if err != nil { return err } sourceRepo := targetRepo if pr.TargetRepoID != pr.SourceRepoID { sourceRepo, err = s.repoGitInfoCache.Get(ctx, pr.SourceRepoID) if err != nil { return err } } writeParams, err := createSystemRPCWriteParams(ctx, s.urlProvider, targetRepo.ID, targetRepo.GitUID) if err != nil { return fmt.Errorf("failed to generate rpc write params: %w", err) } // call merge and store output in pr merge reference. now := time.Now() var output gitrpc.MergeOutput output, err = s.gitRPCClient.Merge(ctx, &gitrpc.MergeParams{ WriteParams: writeParams, BaseBranch: pr.TargetBranch, HeadRepoUID: sourceRepo.GitUID, HeadBranch: pr.SourceBranch, RefType: gitrpcenum.RefTypePullReqMerge, RefName: strconv.Itoa(int(prNum)), HeadExpectedSHA: newSHA, Force: true, // set committer date to ensure repeatability of merge commit across replicas CommitterDate: &now, }) if gitrpc.ErrorStatus(err) == gitrpc.StatusPreconditionFailed { return events.NewDiscardEventErrorf("Source branch '%s' is not on SHA '%s' anymore.", pr.SourceBranch, newSHA) } isNotMergeableError := gitrpc.ErrorStatus(err) == gitrpc.StatusNotMergeable if err != nil && !isNotMergeableError { return fmt.Errorf("merge check failed for %d:%s and %d:%s with err: %w", targetRepo.ID, pr.TargetBranch, sourceRepo.ID, pr.SourceBranch, err) } // Update DB in both cases (failure or success) _, err = s.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error { if pr.SourceSHA != newSHA { return events.NewDiscardEventErrorf("PR SHA %s is newer than %s", pr.SourceSHA, newSHA) } if isNotMergeableError { // TODO: gitrpc should return sha's either way, and also conflicting files! pr.MergeCheckStatus = enum.MergeCheckStatusConflict pr.MergeTargetSHA = &output.BaseSHA pr.MergeSHA = nil pr.MergeConflicts = nil } else { pr.MergeCheckStatus = enum.MergeCheckStatusMergeable pr.MergeTargetSHA = &output.BaseSHA pr.MergeBaseSHA = output.MergeBaseSHA // TODO: Merge check should not update the merge base. pr.MergeSHA = &output.MergeSHA pr.MergeConflicts = nil } return nil }) if err != nil { return fmt.Errorf("failed to update PR merge ref in db with error: %w", err) } return nil }