drone/app/services/pullreq/handlers_branch.go

282 lines
9.1 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pullreq
import (
"context"
"errors"
"fmt"
"strings"
"time"
gitevents "github.com/harness/gitness/app/events/git"
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/events"
"github.com/harness/gitness/git"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
var (
errPRNotOpen = errors.New("PR is not open")
)
// triggerPREventOnBranchUpdate handles branch update events. For every open pull request
// it writes an activity entry and triggers the pull request Branch Updated event.
//
//nolint:gocognit // refactor if needed
func (s *Service) triggerPREventOnBranchUpdate(ctx context.Context,
event *events.Event[*gitevents.BranchUpdatedPayload],
) error {
// we should always update PR mergeable status check when target branch is updated.
// - main
// |- develop
// |- feature1
// |- feature2
// when feature2 merge changes into develop branch then feature1 branch is not consistent anymore
// and need to run mergeable check even nothing was changed on feature1, same applies to main if someone
// push new commit to main then develop should merge status should be unchecked.
if branch, err := getBranchFromRef(event.Payload.Ref); err == nil {
err = s.pullreqStore.ResetMergeCheckStatus(ctx, event.Payload.RepoID, branch)
if err != nil {
return err
}
}
// TODO: This function is currently executed directly on branch update event.
// TODO: But it should be executed after the PR's head ref has been updated.
// TODO: This is to make sure the commit exists on the target repository for forked repositories.
s.forEveryOpenPR(ctx, event.Payload.RepoID, event.Payload.Ref, func(pr *types.PullReq) error {
// First check if the merge base has changed
targetRepo, err := s.repoGitInfoCache.Get(ctx, pr.TargetRepoID)
if err != nil {
return fmt.Errorf("failed to get repo git info: %w", err)
}
mergeBaseInfo, err := s.git.MergeBase(ctx, git.MergeBaseParams{
ReadParams: git.ReadParams{RepoUID: targetRepo.GitUID},
Ref1: event.Payload.NewSHA,
Ref2: pr.TargetBranch,
})
if err != nil {
return fmt.Errorf("failed to get merge base after branch update to=%s for PR=%d: %w",
event.Payload.NewSHA, pr.Number, err)
}
oldMergeBase := pr.MergeBaseSHA
newMergeBase := mergeBaseInfo.MergeBaseSHA
// Update the database with the latest source commit SHA and the merge base SHA.
pr, err = s.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
// to avoid racing conditions
if pr.State != enum.PullReqStateOpen {
return errPRNotOpen
}
pr.ActivitySeq++
if pr.SourceSHA != event.Payload.OldSHA {
return fmt.Errorf(
"failed to set SourceSHA for PR %d to value '%s', expected SHA '%s' but current pr has '%s'",
pr.Number, event.Payload.NewSHA, event.Payload.OldSHA, pr.SourceSHA)
}
pr.Edited = time.Now().UnixMilli()
pr.SourceSHA = event.Payload.NewSHA
pr.MergeBaseSHA = newMergeBase.String()
// reset merge-check fields for new run
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
pr.MergeSHA = nil
pr.MergeConflicts = nil
pr.Stats.DiffStats.Commits = nil
pr.Stats.DiffStats.FilesChanged = nil
return nil
})
if errors.Is(err, errPRNotOpen) {
return nil
}
if err != nil {
return err
}
payload := &types.PullRequestActivityPayloadBranchUpdate{
Old: event.Payload.OldSHA,
New: event.Payload.NewSHA,
}
_, err = s.activityStore.CreateWithPayload(ctx, pr, event.Payload.PrincipalID, payload)
if err != nil {
// non-critical error
log.Ctx(ctx).Err(err).Msgf("failed to write pull request activity after branch update")
}
s.pullreqEvReporter.BranchUpdated(ctx, &pullreqevents.BranchUpdatedPayload{
Base: pullreqevents.Base{
PullReqID: pr.ID,
SourceRepoID: pr.SourceRepoID,
TargetRepoID: pr.TargetRepoID,
PrincipalID: event.Payload.PrincipalID,
Number: pr.Number,
},
OldSHA: event.Payload.OldSHA,
NewSHA: event.Payload.NewSHA,
OldMergeBaseSHA: oldMergeBase,
NewMergeBaseSHA: newMergeBase.String(),
Forced: event.Payload.Forced,
})
if err = s.sseStreamer.Publish(ctx, targetRepo.ParentID, enum.SSETypePullRequestUpdated, pr); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("failed to publish PR changed event")
}
return nil
})
return nil
}
// closePullReqOnBranchDelete handles branch delete events.
// It closes every open pull request for the branch and triggers the pull request BranchDeleted event.
func (s *Service) closePullReqOnBranchDelete(ctx context.Context,
event *events.Event[*gitevents.BranchDeletedPayload],
) error {
s.forEveryOpenPR(ctx, event.Payload.RepoID, event.Payload.Ref, func(pr *types.PullReq) error {
targetRepo, err := s.repoGitInfoCache.Get(ctx, pr.TargetRepoID)
if err != nil {
return fmt.Errorf("failed to get repo info: %w", err)
}
var activitySeqBranchDeleted, activitySeqPRClosed int64
pr, err = s.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
// to avoid racing conditions
if pr.State != enum.PullReqStateOpen {
return errPRNotOpen
}
// get sequence numbers for both activities (branch deletion should be first)
pr.ActivitySeq += 2
activitySeqBranchDeleted = pr.ActivitySeq - 1
activitySeqPRClosed = pr.ActivitySeq
pr.State = enum.PullReqStateClosed
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
pr.MergeSHA = nil
pr.MergeConflicts = nil
return nil
})
if errors.Is(err, errPRNotOpen) {
return nil
}
if err != nil {
return fmt.Errorf("failed to close pull request after branch delete: %w", err)
}
// NOTE: We use the latest PR source sha for the branch deleted activity.
// There is a chance the PR is behind, but we can't guarantee any missing commit exists after branch deletion.
// Whatever is the source sha of the PR is most likely to be pointed at by the PR head ref.
pr.ActivitySeq = activitySeqBranchDeleted
_, err = s.activityStore.CreateWithPayload(ctx, pr, event.Payload.PrincipalID,
&types.PullRequestActivityPayloadBranchDelete{SHA: pr.SourceSHA})
if err != nil {
// non-critical error
log.Ctx(ctx).Err(err).Msg("failed to write pull request activity for branch deletion")
}
pr.ActivitySeq = activitySeqPRClosed
payload := &types.PullRequestActivityPayloadStateChange{
Old: enum.PullReqStateOpen,
New: enum.PullReqStateClosed,
OldDraft: pr.IsDraft,
NewDraft: pr.IsDraft,
}
if _, err := s.activityStore.CreateWithPayload(ctx, pr, event.Payload.PrincipalID, payload); err != nil {
// non-critical error
log.Ctx(ctx).Err(err).Msg(
"failed to write pull request activity for pullrequest closure after branch deletion",
)
}
s.pullreqEvReporter.Closed(ctx, &pullreqevents.ClosedPayload{
Base: pullreqevents.Base{
PullReqID: pr.ID,
SourceRepoID: pr.SourceRepoID,
TargetRepoID: pr.TargetRepoID,
PrincipalID: event.Payload.PrincipalID,
Number: pr.Number,
},
SourceSHA: pr.SourceSHA,
})
if err = s.sseStreamer.Publish(ctx, targetRepo.ParentID, enum.SSETypePullRequestUpdated, pr); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("failed to publish PR changed event")
}
return nil
})
return nil
}
// forEveryOpenPR is utility function that executes the provided function
// for every open pull request created with the source branch given as a git ref.
func (s *Service) forEveryOpenPR(ctx context.Context,
repoID int64, ref string,
fn func(pr *types.PullReq) error,
) {
const largeLimit = 1000000
branch, err := getBranchFromRef(ref)
if len(branch) == 0 {
log.Ctx(ctx).Err(err).Send()
return
}
pullreqList, err := s.pullreqStore.List(ctx, &types.PullReqFilter{
Page: 0,
Size: largeLimit,
SourceRepoID: repoID,
SourceBranch: branch,
States: []enum.PullReqState{enum.PullReqStateOpen},
Sort: enum.PullReqSortNumber,
Order: enum.OrderAsc,
})
if err != nil {
log.Ctx(ctx).Err(err).Msg("failed to get list of open pull requests")
return
}
for _, pr := range pullreqList {
if err = fn(pr); err != nil {
log.Ctx(ctx).Err(err).Msg("failed to process pull req")
}
}
}
func getBranchFromRef(ref string) (string, error) {
const refPrefix = "refs/heads/"
if !strings.HasPrefix(ref, refPrefix) {
return "", fmt.Errorf("failed to get branch name from branch ref %s", ref)
}
branch := ref[len(refPrefix):]
if len(branch) == 0 {
return "", fmt.Errorf("got an empty branch name from branch ref %s", ref)
}
return branch, nil
}