This commit is contained in:
@@ -11,41 +11,42 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
"gitea.dev/models/pull"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/automergequeue"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
)
|
||||
|
||||
// prPatchCheckerQueue represents a queue to handle update pull request tests
|
||||
var prPatchCheckerQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
var (
|
||||
ErrIsClosed = errors.New("pull is closed")
|
||||
ErrNoPermissionToMerge = errors.New("no permission to merge")
|
||||
ErrNotReadyToMerge = errors.New("not ready to merge")
|
||||
ErrHasMerged = errors.New("has already been merged")
|
||||
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
||||
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
||||
ErrNotMergeableState = errors.New("not in mergeable state")
|
||||
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
||||
ErrIsClosed = errors.New("pull is closed")
|
||||
ErrNoPermissionToMerge = errors.New("no permission to merge")
|
||||
ErrNotReadyToMerge = errors.New("not ready to merge")
|
||||
ErrHasMerged = errors.New("has already been merged")
|
||||
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
||||
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
||||
ErrNotMergeableState = errors.New("not in mergeable state")
|
||||
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
||||
ErrHeadCommitsNotAllVerified = errors.New("the branch requires signed commits but not all head commits are verified")
|
||||
)
|
||||
|
||||
func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool {
|
||||
@@ -132,7 +133,13 @@ const (
|
||||
)
|
||||
|
||||
// CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
|
||||
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminForceMerge bool) error {
|
||||
// mergeStyle tailors the "require signed commits" prechecks:
|
||||
// - fast-forward-only: no Gitea commit is produced, so Gitea's merge-signing check is skipped;
|
||||
// only the user's head commits are verified.
|
||||
// - merge: both the head commits must be verified and Gitea must sign the merge commit.
|
||||
// - rebase, rebase-merge, squash: Gitea rewrites the commits and signs each, so only Gitea's
|
||||
// signing ability is checked.
|
||||
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, mergeStyle repo_model.MergeStyle, forceMerge bool) error {
|
||||
return db.WithTx(stdCtx, func(ctx context.Context) error {
|
||||
if pr.HasMerged {
|
||||
return ErrHasMerged
|
||||
@@ -161,7 +168,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
return ErrIsWorkInProgress
|
||||
}
|
||||
|
||||
if !pr.CanAutoMerge() && !pr.IsEmpty() {
|
||||
if !pr.IsStatusMergeable() && !pr.IsEmpty() {
|
||||
return ErrNotMergeableState
|
||||
}
|
||||
|
||||
@@ -169,21 +176,21 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
return ErrIsChecking
|
||||
}
|
||||
|
||||
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||
if !errors.Is(err, ErrNotReadyToMerge) {
|
||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
||||
return err
|
||||
if errProtection := CheckPullBranchProtections(ctx, pr, false); errProtection != nil {
|
||||
if !errors.Is(errProtection, ErrNotReadyToMerge) {
|
||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, errProtection)
|
||||
return errProtection
|
||||
}
|
||||
|
||||
// Now the branch protection check failed, check whether the failure could be skipped (skip by setting err = nil)
|
||||
|
||||
// * when doing Auto Merge (Scheduled Merge After Checks Succeed), skip the branch protection check
|
||||
if mergeCheckType == MergeCheckTypeAuto {
|
||||
err = nil
|
||||
errProtection = nil
|
||||
}
|
||||
|
||||
// * if admin tries to "Force Merge", they could sometimes skip the branch protection check
|
||||
if adminForceMerge {
|
||||
// * if the doer tries to "Force Merge", check whether it is really allowed
|
||||
if forceMerge {
|
||||
isRepoAdmin, errForceMerge := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer)
|
||||
if errForceMerge != nil {
|
||||
return fmt.Errorf("IsUserRepoAdmin failed, repo: %v, doer: %v, err: %w", pr.BaseRepoID, doer.ID, errForceMerge)
|
||||
@@ -194,20 +201,22 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
return fmt.Errorf("GetFirstMatchProtectedBranchRule failed, repo: %v, base branch: %v, err: %w", pr.BaseRepoID, pr.BaseBranch, errForceMerge)
|
||||
}
|
||||
|
||||
// if doer is admin and the "Force Merge" is not blocked, then clear the branch protection check error
|
||||
blockAdminForceMerge := protectedBranchRule != nil && protectedBranchRule.BlockAdminMergeOverride
|
||||
if isRepoAdmin && !blockAdminForceMerge {
|
||||
err = nil
|
||||
canForceMerge := isRepoAdmin
|
||||
if protectedBranchRule != nil {
|
||||
canForceMerge = git_model.CanBypassBranchProtection(ctx, protectedBranchRule, doer, isRepoAdmin)
|
||||
}
|
||||
if canForceMerge {
|
||||
errProtection = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there is still a branch protection check error, return it
|
||||
if err != nil {
|
||||
return err
|
||||
if errProtection != nil {
|
||||
return errProtection
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := isSignedIfRequired(ctx, pr, doer); err != nil {
|
||||
if err := checkSigningRequirements(ctx, pr, doer, mergeStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -221,26 +230,45 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
})
|
||||
}
|
||||
|
||||
// isSignedIfRequired check if merge will be signed if required
|
||||
func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
|
||||
// checkSigningRequirements enforces the target branch's RequireSignedCommits rule
|
||||
// against the selected merge style:
|
||||
// - fast-forward-only and merge keep the user's commits on the base branch, so
|
||||
// those commits must all be verified, or the pre-receive hook will reject the
|
||||
// push with a generic error.
|
||||
// - fast-forward-only creates no Gitea commit, so Gitea's signing key is not used.
|
||||
// - merge, rebase, rebase-merge and squash produce a Gitea-signed commit, so
|
||||
// Gitea must be configured to sign it.
|
||||
func checkSigningRequirements(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle) error {
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
if pb == nil || !pb.RequireSignedCommits {
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo)
|
||||
if mergeStyle == repo_model.MergeStyleFastForwardOnly || mergeStyle == repo_model.MergeStyleMerge {
|
||||
verified, err := asymkey_service.AllHeadCommitsVerified(ctx, pr, gitRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !verified {
|
||||
return ErrHeadCommitsNotAllVerified
|
||||
}
|
||||
}
|
||||
|
||||
return sign, err
|
||||
if mergeStyle != repo_model.MergeStyleFastForwardOnly {
|
||||
if _, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
|
||||
|
||||
Reference in New Issue
Block a user