forgejo/services/pull/commit_status.go
Markus Amshove b1d66f50fb
Disallow merge when required checked are missing (#29143)
fixes #21892

This PR disallows merging a PR when not all commit status contexts
configured in the branch protection are met.

Previously, the PR was happy to merge when one commit status was
successful and the other contexts weren't reported.

Any feedback is welcome, first time Go :-)
I'm also not sure if the changes in the template break something else

Given the following branch protection:

![branch_protection](https://github.com/go-gitea/gitea/assets/2401875/f871b4e4-138b-435a-b496-f9ad432e3dec)

This was shown before the change:

![before](https://github.com/go-gitea/gitea/assets/2401875/60424ff0-ee09-4fa0-856e-64e6e3fb0612)

With the change, it is now shown as this:

![after](https://github.com/go-gitea/gitea/assets/2401875/4e464142-efb1-4889-8166-eb3be26c8f3d)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
(cherry picked from commit a11ccc9fcd61fb25ffb1c37b87a0df4ee9efd84e)
2024-02-19 22:58:33 +01:00

167 lines
4.9 KiB
Go

// Copyright 2019 The Gitea Authors.
// All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"errors"
"fmt"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"github.com/gobwas/glob"
)
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState {
// matchedCount is the number of `CommitStatus.Context` that match any context of `requiredContexts`
matchedCount := 0
returnedStatus := structs.CommitStatusSuccess
if len(requiredContexts) > 0 {
requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
for _, ctx := range requiredContexts {
if gp, err := glob.Compile(ctx); err != nil {
log.Error("glob.Compile %s failed. Error: %v", ctx, err)
} else {
requiredContextsGlob[ctx] = gp
}
}
for _, commitStatus := range commitStatuses {
var targetStatus structs.CommitStatusState
for _, gp := range requiredContextsGlob {
if gp.Match(commitStatus.Context) {
targetStatus = commitStatus.State
matchedCount++
break
}
}
if targetStatus != "" && targetStatus.NoBetterThan(returnedStatus) {
returnedStatus = targetStatus
}
}
}
if matchedCount != len(requiredContexts) {
return structs.CommitStatusPending
}
if matchedCount == 0 {
status := git_model.CalcCommitStatus(commitStatuses)
if status != nil {
return status.State
}
return structs.CommitStatusSuccess
}
return returnedStatus
}
// IsCommitStatusContextSuccess returns true if all required status check contexts succeed.
func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requiredContexts []string) bool {
// If no specific context is required, require that last commit status is a success
if len(requiredContexts) == 0 {
status := git_model.CalcCommitStatus(commitStatuses)
if status == nil || status.State != structs.CommitStatusSuccess {
return false
}
return true
}
for _, ctx := range requiredContexts {
var found bool
for _, commitStatus := range commitStatuses {
if commitStatus.Context == ctx {
if commitStatus.State != structs.CommitStatusSuccess {
return false
}
found = true
break
}
}
if !found {
return false
}
}
return true
}
// IsPullCommitStatusPass returns if all required status checks PASS
func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
}
if pb == nil || !pb.EnableStatusCheck {
return true, nil
}
state, err := GetPullRequestCommitStatusState(ctx, pr)
if err != nil {
return false, err
}
return state.IsSuccess(), nil
}
// GetPullRequestCommitStatusState returns pull request merged commit status state
func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
// Ensure HeadRepo is loaded
if err := pr.LoadHeadRepo(ctx); err != nil {
return "", fmt.Errorf("LoadHeadRepo: %w", err)
}
// check if all required status checks are successful
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
if err != nil {
return "", fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
}
defer closer.Close()
if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
return "", errors.New("head branch does not exist, can not merge")
}
if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
return "", errors.New("head branch does not exist, can not merge")
}
var sha string
if pr.Flow == issues_model.PullRequestFlowGithub {
sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
} else {
sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName())
}
if err != nil {
return "", err
}
if err := pr.LoadBaseRepo(ctx); err != nil {
return "", fmt.Errorf("LoadBaseRepo: %w", err)
}
commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
if err != nil {
return "", fmt.Errorf("GetLatestCommitStatus: %w", err)
}
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return "", fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
}
var requiredContexts []string
if pb != nil {
requiredContexts = pb.StatusCheckContexts
}
return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil
}