Skip to content

Commit 854b8a8

Browse files
committed
tide: changes requested block merge when branch protection requires reviews
1 parent 8026e80 commit 854b8a8

File tree

5 files changed

+616
-7
lines changed

5 files changed

+616
-7
lines changed

pkg/tide/github.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -529,16 +529,18 @@ type mergeChecker struct {
529529
ghc githubClient
530530

531531
sync.Mutex
532-
cache map[config.OrgRepo]map[types.PullRequestMergeType]bool
532+
cache map[config.OrgRepo]map[types.PullRequestMergeType]bool
533+
branchProtectionReviewsCache map[string]bool
533534
}
534535

535536
// newMergeChecker creates a mergeChecker for GitHub, and should be used only by
536537
// GitHubProvider.
537538
func newMergeChecker(cfg config.Getter, ghc githubClient) *mergeChecker {
538539
m := &mergeChecker{
539-
config: cfg,
540-
ghc: ghc,
541-
cache: map[config.OrgRepo]map[types.PullRequestMergeType]bool{},
540+
config: cfg,
541+
ghc: ghc,
542+
cache: map[config.OrgRepo]map[types.PullRequestMergeType]bool{},
543+
branchProtectionReviewsCache: map[string]bool{},
542544
}
543545

544546
go m.clearCache()
@@ -554,6 +556,7 @@ func (m *mergeChecker) clearCache() {
554556
<-ticker.C
555557
m.Lock()
556558
m.cache = make(map[config.OrgRepo]map[types.PullRequestMergeType]bool)
559+
m.branchProtectionReviewsCache = map[string]bool{}
557560
m.Unlock()
558561
}
559562
}
@@ -588,6 +591,36 @@ func (m *mergeChecker) repoMethods(orgRepo config.OrgRepo) (map[types.PullReques
588591
return repoMethods, nil
589592
}
590593

594+
// branchRequiresReviews checks if branch protection requires pull request reviews.
595+
func (m *mergeChecker) branchRequiresReviews(org, repo, branch string) (bool, error) {
596+
cacheKey := fmt.Sprintf("%s/%s:%s", org, repo, branch)
597+
m.Lock()
598+
cached, ok := m.branchProtectionReviewsCache[cacheKey]
599+
m.Unlock()
600+
if ok {
601+
return cached, nil
602+
}
603+
604+
bp, err := m.ghc.GetBranchProtection(org, repo, branch)
605+
if err != nil {
606+
// If branch protection is not configured or we can't access it,
607+
// assume reviews are not required (GitHub's default).
608+
// Cache the result to avoid repeated API calls for the same branch.
609+
m.Lock()
610+
m.branchProtectionReviewsCache[cacheKey] = false
611+
m.Unlock()
612+
return false, nil
613+
}
614+
615+
requiresReviews := bp.RequiredPullRequestReviews != nil &&
616+
bp.RequiredPullRequestReviews.RequiredApprovingReviewCount > 0
617+
618+
m.Lock()
619+
m.branchProtectionReviewsCache[cacheKey] = requiresReviews
620+
m.Unlock()
621+
return requiresReviews, nil
622+
}
623+
591624
// isAllowed checks if a PR does not have merge conflicts and requests an
592625
// allowed merge method. If there is no error it returns a string explanation if
593626
// not allowed or "" if allowed.
@@ -621,6 +654,19 @@ func (m *mergeChecker) isAllowedToMerge(crc *CodeReviewCommon) (string, error) {
621654
} else if !allowed {
622655
return fmt.Sprintf("Merge type %q disallowed by repo settings", *mergeMethod), nil
623656
}
657+
// Check if PR has "changes requested" review state and branch protection requires reviews.
658+
if pr.ReviewDecision == githubql.PullRequestReviewDecisionChangesRequested {
659+
requiresReviews, err := m.branchRequiresReviews(crc.Org, crc.Repo, crc.BaseRefName)
660+
if err != nil {
661+
logrus.WithError(err).WithFields(logrus.Fields{
662+
"org": crc.Org,
663+
"repo": crc.Repo,
664+
"branch": crc.BaseRefName,
665+
}).Debug("Error checking if branch requires reviews, allowing GitHub to enforce")
666+
} else if requiresReviews {
667+
return "PR has changes requested in reviews and branch protection requires reviews", nil
668+
}
669+
}
624670
return "", nil
625671
}
626672

pkg/tide/status.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ func (sc *statusController) shutdown() {
125125
// Note: an empty diff can be returned if the reason that the PR does not match
126126
// the TideQuery is unknown. This can happen if this function's logic
127127
// does not match GitHub's and does not indicate that the PR matches the query.
128-
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (string, int) {
128+
// branchRequiresReviews is an optional function that checks if branch protection
129+
// requires reviews. If nil, "changes requested" will always be treated as not blocking.
130+
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker, branchRequiresReviews func(string, string, string) (bool, error)) (string, int) {
129131
const maxLabelChars = 50
130132
var desc string
131133
var diff int
@@ -254,7 +256,21 @@ func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (s
254256
}
255257
}
256258

257-
if q.ReviewApprovedRequired && pr.ReviewDecision != githubql.PullRequestReviewDecisionApproved {
259+
// PullRequestReviewDecisionChangesRequested blocks merging only if branch protection requires reviews.
260+
if pr.ReviewDecision == githubql.PullRequestReviewDecisionChangesRequested {
261+
shouldBlock := false
262+
if branchRequiresReviews != nil {
263+
if requiresReviews, err := branchRequiresReviews(string(pr.Repository.Owner.Login), string(pr.Repository.Name), string(pr.BaseRef.Name)); err == nil {
264+
shouldBlock = requiresReviews
265+
}
266+
}
267+
if shouldBlock {
268+
diff += 50
269+
if desc == "" {
270+
desc = " PR has changes requested in reviews"
271+
}
272+
}
273+
} else if q.ReviewApprovedRequired && pr.ReviewDecision != githubql.PullRequestReviewDecisionApproved {
258274
diff += 50
259275
if desc == "" {
260276
desc = " PullRequest is missing sufficient approving GitHub review(s)"
@@ -314,8 +330,11 @@ func (sc *statusController) expectedStatus(log *logrus.Entry, queryMap *config.Q
314330

315331
minDiffCount := -1
316332
var minDiff string
333+
branchRequiresReviewsFunc := func(org, repo, branch string) (bool, error) {
334+
return sc.ghProvider.mergeChecker.branchRequiresReviews(org, repo, branch)
335+
}
317336
for _, q := range queryMap.ForRepo(repo) {
318-
diff, diffCount := requirementDiff(pr, &q, cc)
337+
diff, diffCount := requirementDiff(pr, &q, cc, branchRequiresReviewsFunc)
319338
if diffCount == 0 {
320339
hasFulfilledQuery = true
321340
break

0 commit comments

Comments
 (0)