Allow repo admin to merge PR regardless of review status (#9611)
* Allow repo admin to merge even if review is not ok.
This commit is contained in:
		
							parent
							
								
									4d06d10dba
								
							
						
					
					
						commit
						32fb813133
					
				@ -93,8 +93,8 @@ func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
 | 
				
			|||||||
	return in
 | 
						return in
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CanUserMerge returns if some user could merge a pull request to this protected branch
 | 
					// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
 | 
				
			||||||
func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
 | 
					func (protectBranch *ProtectedBranch) IsUserMergeWhitelisted(userID int64) bool {
 | 
				
			||||||
	if !protectBranch.EnableMergeWhitelist {
 | 
						if !protectBranch.EnableMergeWhitelist {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -348,27 +348,6 @@ func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User)
 | 
				
			|||||||
	return false, nil
 | 
						return false, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IsProtectedBranchForMerging checks if branch is protected for merging
 | 
					 | 
				
			||||||
func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName string, doer *User) (bool, error) {
 | 
					 | 
				
			||||||
	if doer == nil {
 | 
					 | 
				
			||||||
		return true, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protectedBranch := &ProtectedBranch{
 | 
					 | 
				
			||||||
		RepoID:     repo.ID,
 | 
					 | 
				
			||||||
		BranchName: branchName,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	has, err := x.Get(protectedBranch)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return true, err
 | 
					 | 
				
			||||||
	} else if has {
 | 
					 | 
				
			||||||
		return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr) || protectedBranch.MergeBlockedByRejectedReview(pr), nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return false, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
 | 
					// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
 | 
				
			||||||
// the users from newWhitelist which have explicit read or write access to the repo.
 | 
					// the users from newWhitelist which have explicit read or write access to the repo.
 | 
				
			||||||
func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
 | 
					func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
 | 
				
			||||||
 | 
				
			|||||||
@ -479,31 +479,6 @@ const (
 | 
				
			|||||||
	MergeStyleSquash MergeStyle = "squash"
 | 
						MergeStyleSquash MergeStyle = "squash"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CheckUserAllowedToMerge checks whether the user is allowed to merge
 | 
					 | 
				
			||||||
func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
 | 
					 | 
				
			||||||
	if doer == nil {
 | 
					 | 
				
			||||||
		return ErrNotAllowedToMerge{
 | 
					 | 
				
			||||||
			"Not signed in",
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if pr.BaseRepo == nil {
 | 
					 | 
				
			||||||
		if err = pr.GetBaseRepo(); err != nil {
 | 
					 | 
				
			||||||
			return fmt.Errorf("GetBaseRepo: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr, pr.BaseBranch, doer); err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("IsProtectedBranch: %v", err)
 | 
					 | 
				
			||||||
	} else if protected {
 | 
					 | 
				
			||||||
		return ErrNotAllowedToMerge{
 | 
					 | 
				
			||||||
			"The branch is protected",
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SetMerged sets a pull request to merged and closes the corresponding issue
 | 
					// SetMerged sets a pull request to merged and closes the corresponding issue
 | 
				
			||||||
func (pr *PullRequest) SetMerged() (err error) {
 | 
					func (pr *PullRequest) SetMerged() (err error) {
 | 
				
			||||||
	if pr.HasMerged {
 | 
						if pr.HasMerged {
 | 
				
			||||||
 | 
				
			|||||||
@ -271,7 +271,7 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
 | 
				
			|||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IsUserRepoAdmin return ture if user has admin right of a repo
 | 
					// IsUserRepoAdmin return true if user has admin right of a repo
 | 
				
			||||||
func IsUserRepoAdmin(repo *Repository, user *User) (bool, error) {
 | 
					func IsUserRepoAdmin(repo *Repository, user *User) (bool, error) {
 | 
				
			||||||
	return isUserRepoAdmin(x, repo, user)
 | 
						return isUserRepoAdmin(x, repo, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -448,6 +448,7 @@ type MergePullRequestForm struct {
 | 
				
			|||||||
	Do                string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
 | 
						Do                string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
 | 
				
			||||||
	MergeTitleField   string
 | 
						MergeTitleField   string
 | 
				
			||||||
	MergeMessageField string
 | 
						MergeMessageField string
 | 
				
			||||||
 | 
						ForceMerge        *bool `json:"force_merge,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate validates the fields
 | 
					// Validate validates the fields
 | 
				
			||||||
 | 
				
			|||||||
@ -51,7 +51,7 @@ func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.
 | 
				
			|||||||
		EnableStatusCheck:   bp.EnableStatusCheck,
 | 
							EnableStatusCheck:   bp.EnableStatusCheck,
 | 
				
			||||||
		StatusCheckContexts: bp.StatusCheckContexts,
 | 
							StatusCheckContexts: bp.StatusCheckContexts,
 | 
				
			||||||
		UserCanPush:         bp.CanUserPush(user.ID),
 | 
							UserCanPush:         bp.CanUserPush(user.ID),
 | 
				
			||||||
		UserCanMerge:        bp.CanUserMerge(user.ID),
 | 
							UserCanMerge:        bp.IsUserMergeWhitelisted(user.ID),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1062,7 +1062,8 @@ pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
 | 
				
			|||||||
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
					pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 | 
				
			||||||
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
					pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
 | 
				
			||||||
pulls.no_merge_wip = This pull request can not be merged because it is marked as being a work in progress.
 | 
					pulls.no_merge_wip = This pull request can not be merged because it is marked as being a work in progress.
 | 
				
			||||||
pulls.no_merge_status_check = This pull request cannot be merged because not all required status checkes are successful.
 | 
					pulls.no_merge_not_ready = This pull request is not ready to be merged, check review status and status checks.
 | 
				
			||||||
 | 
					pulls.no_merge_access = You are not authorized to merge this pull request.
 | 
				
			||||||
pulls.merge_pull_request = Merge Pull Request
 | 
					pulls.merge_pull_request = Merge Pull Request
 | 
				
			||||||
pulls.rebase_merge_pull_request = Rebase and Merge
 | 
					pulls.rebase_merge_pull_request = Rebase and Merge
 | 
				
			||||||
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
 | 
					pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
 | 
				
			||||||
 | 
				
			|||||||
@ -600,20 +600,43 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						perm, err := models.GetUserRepoPermission(ctx.Repo.Repository, ctx.User)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, ctx.User)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "IsUSerAllowedToMerge", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !allowedMerge {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !pr.CanAutoMerge() || pr.HasMerged || pr.IsWorkInProgress() {
 | 
						if !pr.CanAutoMerge() || pr.HasMerged || pr.IsWorkInProgress() {
 | 
				
			||||||
		ctx.Status(http.StatusMethodNotAllowed)
 | 
							ctx.Status(http.StatusMethodNotAllowed)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	isPass, err := pull_service.IsPullCommitStatusPass(pr)
 | 
						if err := pull_service.CheckPRReadyToMerge(pr); err != nil {
 | 
				
			||||||
	if err != nil {
 | 
							if !models.IsErrNotAllowedToMerge(err) {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "IsPullCommitStatusPass", err)
 | 
								ctx.Error(http.StatusInternalServerError, "CheckPRReadyToMerge", err)
 | 
				
			||||||
		return
 | 
								return
 | 
				
			||||||
	}
 | 
							}
 | 
				
			||||||
 | 
							if form.ForceMerge != nil && *form.ForceMerge {
 | 
				
			||||||
	if !isPass && !ctx.IsUserRepoAdmin() {
 | 
								if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
 | 
				
			||||||
		ctx.Status(http.StatusMethodNotAllowed)
 | 
									ctx.Error(http.StatusInternalServerError, "IsUserRepoAdmin", err)
 | 
				
			||||||
		return
 | 
									return
 | 
				
			||||||
 | 
								} else if !isRepoAdmin {
 | 
				
			||||||
 | 
									ctx.Error(http.StatusMethodNotAllowed, "Merge", "Only repository admin can merge if not all checks are ok (force merge)")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(form.Do) == 0 {
 | 
						if len(form.Do) == 0 {
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/private"
 | 
						"code.gitea.io/gitea/modules/private"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/repofiles"
 | 
						"code.gitea.io/gitea/modules/repofiles"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						pull_service "code.gitea.io/gitea/services/pull"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"gitea.com/macaron/macaron"
 | 
						"gitea.com/macaron/macaron"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -98,6 +99,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
 | 
				
			|||||||
				canPush = protectBranch.CanUserPush(opts.UserID)
 | 
									canPush = protectBranch.CanUserPush(opts.UserID)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if !canPush && opts.ProtectedBranchID > 0 {
 | 
								if !canPush && opts.ProtectedBranchID > 0 {
 | 
				
			||||||
 | 
									// Manual merge
 | 
				
			||||||
				pr, err := models.GetPullRequestByID(opts.ProtectedBranchID)
 | 
									pr, err := models.GetPullRequestByID(opts.ProtectedBranchID)
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
					log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err)
 | 
										log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err)
 | 
				
			||||||
@ -106,24 +108,55 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
 | 
				
			|||||||
					})
 | 
										})
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				if !protectBranch.HasEnoughApprovals(pr) {
 | 
									user, err := models.GetUserByID(opts.UserID)
 | 
				
			||||||
					log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", opts.UserID, branchName, repo, pr.Index)
 | 
									if err != nil {
 | 
				
			||||||
					ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
										log.Error("Unable to get User id %d Error: %v", opts.UserID, err)
 | 
				
			||||||
						"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, opts.ProtectedBranchID),
 | 
										ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
				
			||||||
 | 
											"err": fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err),
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				if protectBranch.MergeBlockedByRejectedReview(pr) {
 | 
									perm, err := models.GetUserRepoPermission(repo, user)
 | 
				
			||||||
					log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d has requested changes", opts.UserID, branchName, repo, pr.Index)
 | 
									if err != nil {
 | 
				
			||||||
					ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
										log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err)
 | 
				
			||||||
						"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d has requested changes", branchName, opts.ProtectedBranchID),
 | 
										ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
				
			||||||
 | 
											"err": fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err),
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
					return
 | 
										return
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										log.Error("Error calculating if allowed to merge: %v", err)
 | 
				
			||||||
 | 
										ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
				
			||||||
 | 
											"err": fmt.Sprintf("Error calculating if allowed to merge: %v", err),
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !allowedMerge {
 | 
				
			||||||
 | 
										log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index)
 | 
				
			||||||
 | 
										ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
				
			||||||
 | 
											"err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									// Manual merge only allowed if PR is ready (even if admin)
 | 
				
			||||||
 | 
									if err := pull_service.CheckPRReadyToMerge(pr); err != nil {
 | 
				
			||||||
 | 
										if models.IsErrNotAllowedToMerge(err) {
 | 
				
			||||||
 | 
											log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error())
 | 
				
			||||||
 | 
											ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
				
			||||||
 | 
												"err": fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.ProtectedBranchID, err.Error()),
 | 
				
			||||||
 | 
											})
 | 
				
			||||||
 | 
											return
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err)
 | 
				
			||||||
 | 
										ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
				
			||||||
 | 
											"err": fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.ProtectedBranchID, err),
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			} else if !canPush {
 | 
								} else if !canPush {
 | 
				
			||||||
				log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", opts.UserID, branchName, repo)
 | 
									log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo)
 | 
				
			||||||
				ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
									ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
				
			||||||
					"err": fmt.Sprintf("protected branch %s can not be pushed to", branchName),
 | 
										"err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -903,6 +903,7 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
		pull := issue.PullRequest
 | 
							pull := issue.PullRequest
 | 
				
			||||||
		pull.Issue = issue
 | 
							pull.Issue = issue
 | 
				
			||||||
		canDelete := false
 | 
							canDelete := false
 | 
				
			||||||
 | 
							ctx.Data["AllowMerge"] = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ctx.IsSigned {
 | 
							if ctx.IsSigned {
 | 
				
			||||||
			if err := pull.GetHeadRepo(); err != nil {
 | 
								if err := pull.GetHeadRepo(); err != nil {
 | 
				
			||||||
@ -923,6 +924,20 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := pull.GetBaseRepo(); err != nil {
 | 
				
			||||||
 | 
									log.Error("GetBaseRepo: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("GetUserRepoPermission", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									ctx.ServerError("IsUserAllowedToMerge", err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
 | 
							prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
 | 
				
			||||||
@ -932,15 +947,6 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		prConfig := prUnit.PullRequestsConfig()
 | 
							prConfig := prUnit.PullRequestsConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.Data["AllowMerge"] = ctx.Repo.CanWrite(models.UnitTypeCode)
 | 
					 | 
				
			||||||
		if err := pull.CheckUserAllowedToMerge(ctx.User); err != nil {
 | 
					 | 
				
			||||||
			if !models.IsErrNotAllowedToMerge(err) {
 | 
					 | 
				
			||||||
				ctx.ServerError("CheckUserAllowedToMerge", err)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			ctx.Data["AllowMerge"] = false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Check correct values and select default
 | 
							// Check correct values and select default
 | 
				
			||||||
		if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
 | 
							if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
 | 
				
			||||||
			!prConfig.IsMergeStyleAllowed(ms) {
 | 
								!prConfig.IsMergeStyleAllowed(ms) {
 | 
				
			||||||
 | 
				
			|||||||
@ -600,6 +600,16 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	pr := issue.PullRequest
 | 
						pr := issue.PullRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("IsUserAllowedToMerge", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !allowedMerge {
 | 
				
			||||||
 | 
							ctx.NotFound("MergePullRequest", nil)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !pr.CanAutoMerge() || pr.HasMerged {
 | 
						if !pr.CanAutoMerge() || pr.HasMerged {
 | 
				
			||||||
		ctx.NotFound("MergePullRequest", nil)
 | 
							ctx.NotFound("MergePullRequest", nil)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@ -611,15 +621,19 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	isPass, err := pull_service.IsPullCommitStatusPass(pr)
 | 
						if err := pull_service.CheckPRReadyToMerge(pr); err != nil {
 | 
				
			||||||
	if err != nil {
 | 
							if !models.IsErrNotAllowedToMerge(err) {
 | 
				
			||||||
		ctx.ServerError("IsPullCommitStatusPass", err)
 | 
								ctx.ServerError("Merge PR status", err)
 | 
				
			||||||
		return
 | 
								return
 | 
				
			||||||
	}
 | 
							}
 | 
				
			||||||
	if !isPass && !ctx.IsUserRepoAdmin() {
 | 
							if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
 | 
				
			||||||
		ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_status_check"))
 | 
								ctx.ServerError("IsUserRepoAdmin", err)
 | 
				
			||||||
		ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
 | 
								return
 | 
				
			||||||
		return
 | 
							} else if !isRepoAdmin {
 | 
				
			||||||
 | 
								ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
 | 
				
			||||||
 | 
								ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ctx.HasError() {
 | 
						if ctx.HasError() {
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Merge merges pull request to base repository.
 | 
					// Merge merges pull request to base repository.
 | 
				
			||||||
 | 
					// Caller should check PR is ready to be merged (review and status checks)
 | 
				
			||||||
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
 | 
					// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
 | 
				
			||||||
func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
 | 
					func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
 | 
				
			||||||
	binVersion, err := git.BinVersion()
 | 
						binVersion, err := git.BinVersion()
 | 
				
			||||||
@ -53,11 +54,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	prConfig := prUnit.PullRequestsConfig()
 | 
						prConfig := prUnit.PullRequestsConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := pr.CheckUserAllowedToMerge(doer); err != nil {
 | 
					 | 
				
			||||||
		log.Error("CheckUserAllowedToMerge(%v): %v", doer, err)
 | 
					 | 
				
			||||||
		return fmt.Errorf("CheckUserAllowedToMerge: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if merge style is correct and allowed
 | 
						// Check if merge style is correct and allowed
 | 
				
			||||||
	if !prConfig.IsMergeStyleAllowed(mergeStyle) {
 | 
						if !prConfig.IsMergeStyleAllowed(mergeStyle) {
 | 
				
			||||||
		return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
 | 
							return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
 | 
				
			||||||
@ -473,3 +469,64 @@ func getDiffTree(repoPath, baseBranch, headBranch string) (string, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return out.String(), nil
 | 
						return out.String(), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
 | 
				
			||||||
 | 
					func IsUserAllowedToMerge(pr *models.PullRequest, p models.Permission, user *models.User) (bool, error) {
 | 
				
			||||||
 | 
						if p.IsAdmin() {
 | 
				
			||||||
 | 
							return true, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !p.CanWrite(models.UnitTypeCode) {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := pr.LoadProtectedBranch()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if pr.ProtectedBranch == nil || pr.ProtectedBranch.IsUserMergeWhitelisted(user.ID) {
 | 
				
			||||||
 | 
							return true, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CheckPRReadyToMerge checks whether the PR is ready to be merged (reviews and status checks)
 | 
				
			||||||
 | 
					func CheckPRReadyToMerge(pr *models.PullRequest) (err error) {
 | 
				
			||||||
 | 
						if pr.BaseRepo == nil {
 | 
				
			||||||
 | 
							if err = pr.GetBaseRepo(); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("GetBaseRepo: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pr.ProtectedBranch == nil {
 | 
				
			||||||
 | 
							if err = pr.LoadProtectedBranch(); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("LoadProtectedBranch: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if pr.ProtectedBranch == nil {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						isPass, err := IsPullCommitStatusPass(pr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !isPass {
 | 
				
			||||||
 | 
							return models.ErrNotAllowedToMerge{
 | 
				
			||||||
 | 
								Reason: "Not all required status checks successful",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if enoughApprovals := pr.ProtectedBranch.HasEnoughApprovals(pr); !enoughApprovals {
 | 
				
			||||||
 | 
							return models.ErrNotAllowedToMerge{
 | 
				
			||||||
 | 
								Reason: "Does not have enough approvals",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rejected := pr.ProtectedBranch.MergeBlockedByRejectedReview(pr); rejected {
 | 
				
			||||||
 | 
							return models.ErrNotAllowedToMerge{
 | 
				
			||||||
 | 
								Reason: "There are requested changes",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -101,35 +101,31 @@
 | 
				
			|||||||
					<span class="octicon octicon-x"></span>
 | 
										<span class="octicon octicon-x"></span>
 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}}
 | 
										{{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			{{else if .IsBlockedByApprovals}}
 | 
					 | 
				
			||||||
				<div class="item text red">
 | 
					 | 
				
			||||||
					<span class="octicon octicon-x"></span>
 | 
					 | 
				
			||||||
				{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			{{else if .IsBlockedByRejection}}
 | 
					 | 
				
			||||||
				<div class="item text red">
 | 
					 | 
				
			||||||
					<span class="octicon octicon-x"></span>
 | 
					 | 
				
			||||||
				{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			{{else if .Issue.PullRequest.IsChecking}}
 | 
								{{else if .Issue.PullRequest.IsChecking}}
 | 
				
			||||||
				<div class="item text yellow">
 | 
									<div class="item text yellow">
 | 
				
			||||||
					<span class="octicon octicon-sync"></span>
 | 
										<span class="octicon octicon-sync"></span>
 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.is_checking"}}
 | 
										{{$.i18n.Tr "repo.pulls.is_checking"}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			{{else if and (not .Issue.PullRequest.CanAutoMerge) .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
 | 
					 | 
				
			||||||
				<div class="item text red">
 | 
					 | 
				
			||||||
					<span class="octicon octicon-x"></span>
 | 
					 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			{{else if .Issue.PullRequest.CanAutoMerge}}
 | 
								{{else if .Issue.PullRequest.CanAutoMerge}}
 | 
				
			||||||
				{{if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
 | 
									{{if .IsBlockedByApprovals}}
 | 
				
			||||||
				<div class="item text red">
 | 
										<div class="item text red">
 | 
				
			||||||
					<span class="octicon octicon-x"></span>
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
 | 
										{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
 | 
				
			||||||
				</div>
 | 
										</div>
 | 
				
			||||||
 | 
									{{else if .IsBlockedByRejection}}
 | 
				
			||||||
 | 
										<div class="item text red">
 | 
				
			||||||
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
 | 
										{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
 | 
				
			||||||
 | 
										<div class="item text red">
 | 
				
			||||||
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
 | 
											{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
				{{if or $.IsRepoAdmin (not .EnableStatusCheck) .IsRequiredStatusCheckSuccess}}
 | 
									{{$notAllOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}}	
 | 
				
			||||||
					{{if and $.IsRepoAdmin .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
 | 
									{{if or $.IsRepoAdmin (not $notAllOk)}}
 | 
				
			||||||
 | 
										{{if $notAllOk}}
 | 
				
			||||||
						<div class="item text yellow">
 | 
											<div class="item text yellow">
 | 
				
			||||||
							<span class="octicon octicon-primitive-dot"></span>
 | 
												<span class="octicon octicon-primitive-dot"></span>
 | 
				
			||||||
							{{$.i18n.Tr "repo.pulls.required_status_check_administrator"}}
 | 
												{{$.i18n.Tr "repo.pulls.required_status_check_administrator"}}
 | 
				
			||||||
@ -216,7 +212,7 @@
 | 
				
			|||||||
								</form>
 | 
													</form>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
							<div class="ui green buttons merge-button">
 | 
												<div class="ui {{if $notAllOk}}red{{else}}green{{end}} buttons merge-button">
 | 
				
			||||||
								<button class="ui button" data-do="{{.MergeStyle}}">
 | 
													<button class="ui button" data-do="{{.MergeStyle}}">
 | 
				
			||||||
									<span class="octicon octicon-git-merge"></span>
 | 
														<span class="octicon octicon-git-merge"></span>
 | 
				
			||||||
									<span class="button-text">
 | 
														<span class="button-text">
 | 
				
			||||||
@ -262,17 +258,40 @@
 | 
				
			|||||||
								{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
 | 
													{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
 | 
										{{else}}
 | 
				
			||||||
 | 
											<div class="item text grey">
 | 
				
			||||||
 | 
												<span class="octicon octicon-info"></span>
 | 
				
			||||||
 | 
												{{$.i18n.Tr "repo.pulls.no_merge_access"}}
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
			{{else}}
 | 
								{{else}}
 | 
				
			||||||
				<div class="item text red">
 | 
									{{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}}
 | 
				
			||||||
					<span class="octicon octicon-x"></span>
 | 
									{{if .IsBlockedByApprovals}}
 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.cannot_auto_merge_desc"}}
 | 
										<div class="item text red">
 | 
				
			||||||
				</div>
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
				<div class="item text grey">
 | 
										{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
 | 
				
			||||||
					<span class="octicon octicon-info"></span>
 | 
										</div>
 | 
				
			||||||
					{{$.i18n.Tr "repo.pulls.cannot_auto_merge_helper"}}
 | 
									{{else if .IsBlockedByRejection}}
 | 
				
			||||||
				</div>
 | 
										<div class="item text red">
 | 
				
			||||||
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
 | 
										{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
 | 
				
			||||||
 | 
										<div class="item text red">
 | 
				
			||||||
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
 | 
											{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{else}}
 | 
				
			||||||
 | 
										<div class="item text red">
 | 
				
			||||||
 | 
											<span class="octicon octicon-x"></span>
 | 
				
			||||||
 | 
											{{$.i18n.Tr "repo.pulls.cannot_auto_merge_desc"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="item text grey">
 | 
				
			||||||
 | 
											<span class="octicon octicon-info"></span>
 | 
				
			||||||
 | 
											{{$.i18n.Tr "repo.pulls.cannot_auto_merge_helper"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -10745,6 +10745,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "MergeTitleField": {
 | 
					        "MergeTitleField": {
 | 
				
			||||||
          "type": "string"
 | 
					          "type": "string"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "force_merge": {
 | 
				
			||||||
 | 
					          "type": "boolean",
 | 
				
			||||||
 | 
					          "x-go-name": "ForceMerge"
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-name": "MergePullRequestForm",
 | 
					      "x-go-name": "MergePullRequestForm",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user