mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Branch protection: Possibility to not use whitelist but allow anyone with write access (#9055)
* Possibility to not use whitelist but allow anyone with write access * fix existing test * rename migration function * Try to give a better name for migration step * Clear settings if higher level setting is not set * Move official reviews to db instead of counting approvals each time * migration * fix * fix migration * fix migration * Remove NOT NULL from EnableWhitelist as migration isn't possible * Fix migration, reviews are connected to issues. * Fix SQL query issues in GetReviewersByPullID. * Simplify function GetReviewersByIssueID * Handle reviewers that has been deleted * Ensure reviews for test is in a well defined order * Only clear and set official reviews when it is an approve or reject.
This commit is contained in:
		
				
					committed by
					
						
						techknowlogick
					
				
			
			
				
	
			
			
			
						parent
						
							6460284085
						
					
				
				
					commit
					bac4b78e09
				
			@@ -383,6 +383,7 @@ func doProtectBranch(ctx APITestContext, branch string, userToWhitelist string)
 | 
				
			|||||||
			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
 | 
								req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
 | 
				
			||||||
				"_csrf":            csrf,
 | 
									"_csrf":            csrf,
 | 
				
			||||||
				"protected":        "on",
 | 
									"protected":        "on",
 | 
				
			||||||
 | 
									"enable_push":      "whitelist",
 | 
				
			||||||
				"enable_whitelist": "on",
 | 
									"enable_whitelist": "on",
 | 
				
			||||||
				"whitelist_users":  strconv.FormatInt(user.ID, 10),
 | 
									"whitelist_users":  strconv.FormatInt(user.ID, 10),
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,7 @@ type ProtectedBranch struct {
 | 
				
			|||||||
	MergeWhitelistTeamIDs     []int64            `xorm:"JSON TEXT"`
 | 
						MergeWhitelistTeamIDs     []int64            `xorm:"JSON TEXT"`
 | 
				
			||||||
	EnableStatusCheck         bool               `xorm:"NOT NULL DEFAULT false"`
 | 
						EnableStatusCheck         bool               `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
	StatusCheckContexts       []string           `xorm:"JSON TEXT"`
 | 
						StatusCheckContexts       []string           `xorm:"JSON TEXT"`
 | 
				
			||||||
 | 
						EnableApprovalsWhitelist  bool               `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
	ApprovalsWhitelistUserIDs []int64            `xorm:"JSON TEXT"`
 | 
						ApprovalsWhitelistUserIDs []int64            `xorm:"JSON TEXT"`
 | 
				
			||||||
	ApprovalsWhitelistTeamIDs []int64            `xorm:"JSON TEXT"`
 | 
						ApprovalsWhitelistTeamIDs []int64            `xorm:"JSON TEXT"`
 | 
				
			||||||
	RequiredApprovals         int64              `xorm:"NOT NULL DEFAULT 0"`
 | 
						RequiredApprovals         int64              `xorm:"NOT NULL DEFAULT 0"`
 | 
				
			||||||
@@ -53,10 +54,25 @@ func (protectBranch *ProtectedBranch) IsProtected() bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CanUserPush returns if some user could push to this protected branch
 | 
					// CanUserPush returns if some user could push to this protected branch
 | 
				
			||||||
func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
 | 
					func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
 | 
				
			||||||
	if !protectBranch.EnableWhitelist {
 | 
						if !protectBranch.CanPush {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !protectBranch.EnableWhitelist {
 | 
				
			||||||
 | 
							if user, err := GetUserByID(userID); err != nil {
 | 
				
			||||||
 | 
								log.Error("GetUserByID: %v", err)
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							} else if repo, err := GetRepositoryByID(protectBranch.RepoID); err != nil {
 | 
				
			||||||
 | 
								log.Error("GetRepositoryByID: %v", err)
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							} else if writeAccess, err := HasAccessUnit(user, repo, UnitTypeCode, AccessModeWrite); err != nil {
 | 
				
			||||||
 | 
								log.Error("HasAccessUnit: %v", err)
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return writeAccess
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
 | 
						if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -95,6 +111,38 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
 | 
				
			|||||||
	return in
 | 
						return in
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
 | 
				
			||||||
 | 
					func (protectBranch *ProtectedBranch) IsUserOfficialReviewer(user *User) (bool, error) {
 | 
				
			||||||
 | 
						return protectBranch.isUserOfficialReviewer(x, user)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (protectBranch *ProtectedBranch) isUserOfficialReviewer(e Engine, user *User) (bool, error) {
 | 
				
			||||||
 | 
						repo, err := getRepositoryByID(e, protectBranch.RepoID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !protectBranch.EnableApprovalsWhitelist {
 | 
				
			||||||
 | 
							// Anyone with write access is considered official reviewer
 | 
				
			||||||
 | 
							writeAccess, err := hasAccessUnit(e, user, repo, UnitTypeCode, AccessModeWrite)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return writeAccess, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) {
 | 
				
			||||||
 | 
							return true, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						inTeam, err := isUserInTeams(e, user.ID, protectBranch.ApprovalsWhitelistTeamIDs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return inTeam, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// HasEnoughApprovals returns true if pr has enough granted approvals.
 | 
					// HasEnoughApprovals returns true if pr has enough granted approvals.
 | 
				
			||||||
func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
 | 
					func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
 | 
				
			||||||
	if protectBranch.RequiredApprovals == 0 {
 | 
						if protectBranch.RequiredApprovals == 0 {
 | 
				
			||||||
@@ -105,30 +153,16 @@ func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
 | 
					// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
 | 
				
			||||||
func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
 | 
					func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest) int64 {
 | 
				
			||||||
	reviews, err := GetReviewersByPullID(pr.IssueID)
 | 
						approvals, err := x.Where("issue_id = ?", pr.Issue.ID).
 | 
				
			||||||
 | 
							And("type = ?", ReviewTypeApprove).
 | 
				
			||||||
 | 
							And("official = ?", true).
 | 
				
			||||||
 | 
							Count(new(Review))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error("GetReviewersByPullID: %v", err)
 | 
							log.Error("GetGrantedApprovalsCount: %v", err)
 | 
				
			||||||
		return 0
 | 
							return 0
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	approvals := int64(0)
 | 
						return approvals
 | 
				
			||||||
	userIDs := make([]int64, 0)
 | 
					 | 
				
			||||||
	for _, review := range reviews {
 | 
					 | 
				
			||||||
		if review.Type != ReviewTypeApprove {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, review.ID) {
 | 
					 | 
				
			||||||
			approvals++
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		userIDs = append(userIDs, review.ID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	approvalTeamCount, err := UsersInTeamsCount(userIDs, protectBranch.ApprovalsWhitelistTeamIDs)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Error("UsersInTeamsCount: %v", err)
 | 
					 | 
				
			||||||
		return 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return approvalTeamCount + approvals
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetProtectedBranchByRepoID getting protected branch by repo ID
 | 
					// GetProtectedBranchByRepoID getting protected branch by repo ID
 | 
				
			||||||
@@ -139,8 +173,12 @@ func GetProtectedBranchByRepoID(repoID int64) ([]*ProtectedBranch, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// GetProtectedBranchBy getting protected branch by ID/Name
 | 
					// GetProtectedBranchBy getting protected branch by ID/Name
 | 
				
			||||||
func GetProtectedBranchBy(repoID int64, branchName string) (*ProtectedBranch, error) {
 | 
					func GetProtectedBranchBy(repoID int64, branchName string) (*ProtectedBranch, error) {
 | 
				
			||||||
 | 
						return getProtectedBranchBy(x, repoID, branchName)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getProtectedBranchBy(e Engine, repoID int64, branchName string) (*ProtectedBranch, error) {
 | 
				
			||||||
	rel := &ProtectedBranch{RepoID: repoID, BranchName: branchName}
 | 
						rel := &ProtectedBranch{RepoID: repoID, BranchName: branchName}
 | 
				
			||||||
	has, err := x.Get(rel)
 | 
						has, err := e.Get(rel)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,32 +44,32 @@
 | 
				
			|||||||
  reviewer_id: 2
 | 
					  reviewer_id: 2
 | 
				
			||||||
  issue_id: 3
 | 
					  issue_id: 3
 | 
				
			||||||
  content: "New review 3"
 | 
					  content: "New review 3"
 | 
				
			||||||
  updated_unix: 946684810
 | 
					  updated_unix: 946684811
 | 
				
			||||||
  created_unix: 946684810
 | 
					  created_unix: 946684811
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 7
 | 
					  id: 7
 | 
				
			||||||
  type: 3
 | 
					  type: 3
 | 
				
			||||||
  reviewer_id: 3
 | 
					  reviewer_id: 3
 | 
				
			||||||
  issue_id: 3
 | 
					  issue_id: 3
 | 
				
			||||||
  content: "New review 4"
 | 
					  content: "New review 4"
 | 
				
			||||||
  updated_unix: 946684810
 | 
					  updated_unix: 946684812
 | 
				
			||||||
  created_unix: 946684810
 | 
					  created_unix: 946684812
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 8
 | 
					  id: 8
 | 
				
			||||||
  type: 1
 | 
					  type: 1
 | 
				
			||||||
  reviewer_id: 4
 | 
					  reviewer_id: 4
 | 
				
			||||||
  issue_id: 3
 | 
					  issue_id: 3
 | 
				
			||||||
  content: "New review 5"
 | 
					  content: "New review 5"
 | 
				
			||||||
  updated_unix: 946684810
 | 
					  updated_unix: 946684813
 | 
				
			||||||
  created_unix: 946684810
 | 
					  created_unix: 946684813
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 9
 | 
					  id: 9
 | 
				
			||||||
  type: 3
 | 
					  type: 3
 | 
				
			||||||
  reviewer_id: 2
 | 
					  reviewer_id: 2
 | 
				
			||||||
  issue_id: 3
 | 
					  issue_id: 3
 | 
				
			||||||
  content: "New review 3 rejected"
 | 
					  content: "New review 3 rejected"
 | 
				
			||||||
  updated_unix: 946684810
 | 
					  updated_unix: 946684814
 | 
				
			||||||
  created_unix: 946684810
 | 
					  created_unix: 946684814
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 10
 | 
					  id: 10
 | 
				
			||||||
@@ -77,5 +77,5 @@
 | 
				
			|||||||
  reviewer_id: 100
 | 
					  reviewer_id: 100
 | 
				
			||||||
  issue_id: 3
 | 
					  issue_id: 3
 | 
				
			||||||
  content: "a deleted user's review"
 | 
					  content: "a deleted user's review"
 | 
				
			||||||
  updated_unix: 946684810
 | 
					  updated_unix: 946684815
 | 
				
			||||||
  created_unix: 946684810
 | 
					  created_unix: 946684815
 | 
				
			||||||
@@ -276,6 +276,8 @@ var migrations = []Migration{
 | 
				
			|||||||
	NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
 | 
						NewMigration("add can_create_org_repo to team", addCanCreateOrgRepoColumnForTeam),
 | 
				
			||||||
	// v110 -> v111
 | 
						// v110 -> v111
 | 
				
			||||||
	NewMigration("change review content type to text", changeReviewContentToText),
 | 
						NewMigration("change review content type to text", changeReviewContentToText),
 | 
				
			||||||
 | 
						// v111 -> v112
 | 
				
			||||||
 | 
						NewMigration("update branch protection for can push and whitelist enable", addBranchProtectionCanPushAndEnableWhitelist),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Migrate database to current version
 | 
					// Migrate database to current version
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										87
									
								
								models/migrations/v111.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								models/migrations/v111.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					// Copyright 2019 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"xorm.io/xorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type ProtectedBranch struct {
 | 
				
			||||||
 | 
							CanPush                  bool  `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
							EnableApprovalsWhitelist bool  `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
							RequiredApprovals        int64 `xorm:"NOT NULL DEFAULT 0"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type Review struct {
 | 
				
			||||||
 | 
							ID       int64 `xorm:"pk autoincr"`
 | 
				
			||||||
 | 
							Official bool  `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := sess.Sync2(new(ProtectedBranch)); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := sess.Sync2(new(Review)); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := sess.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var pageSize int64 = 20
 | 
				
			||||||
 | 
						qresult, err := sess.QueryInterface("SELECT max(id) as max_id FROM issue")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var totalIssues int64
 | 
				
			||||||
 | 
						totalIssues, ok := qresult[0]["max_id"].(int64)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							// If there are no issues at all we ignore it
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						totalPages := totalIssues / pageSize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Find latest review of each user in each pull request, and set official field if appropriate
 | 
				
			||||||
 | 
						reviews := []*models.Review{}
 | 
				
			||||||
 | 
						var page int64
 | 
				
			||||||
 | 
						for page = 0; page <= totalPages; page++ {
 | 
				
			||||||
 | 
							if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)",
 | 
				
			||||||
 | 
								page*pageSize, (page+1)*pageSize, models.ReviewTypeApprove, models.ReviewTypeReject).
 | 
				
			||||||
 | 
								Find(&reviews); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, review := range reviews {
 | 
				
			||||||
 | 
								if err := review.LoadAttributes(); err != nil {
 | 
				
			||||||
 | 
									// Error might occur if user or issue doesn't exist, ignore it.
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								official, err := models.IsOfficialReviewer(review.Issue, review.Reviewer)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									// Branch might not be proteced or other error, ignore it.
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								review.Official = official
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return sess.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -914,7 +914,11 @@ func RemoveTeamMember(team *Team, userID int64) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// IsUserInTeams returns if a user in some teams
 | 
					// IsUserInTeams returns if a user in some teams
 | 
				
			||||||
func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
 | 
					func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
 | 
				
			||||||
	return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
 | 
						return isUserInTeams(x, userID, teamIDs)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isUserInTeams(e Engine, userID int64, teamIDs []int64) (bool, error) {
 | 
				
			||||||
 | 
						return e.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs
 | 
					// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -159,16 +159,20 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// LoadProtectedBranch loads the protected branch of the base branch
 | 
					// LoadProtectedBranch loads the protected branch of the base branch
 | 
				
			||||||
func (pr *PullRequest) LoadProtectedBranch() (err error) {
 | 
					func (pr *PullRequest) LoadProtectedBranch() (err error) {
 | 
				
			||||||
 | 
						return pr.loadProtectedBranch(x)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) {
 | 
				
			||||||
	if pr.BaseRepo == nil {
 | 
						if pr.BaseRepo == nil {
 | 
				
			||||||
		if pr.BaseRepoID == 0 {
 | 
							if pr.BaseRepoID == 0 {
 | 
				
			||||||
			return nil
 | 
								return nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
 | 
							pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	pr.ProtectedBranch, err = GetProtectedBranchBy(pr.BaseRepo.ID, pr.BaseBranch)
 | 
						pr.ProtectedBranch, err = getProtectedBranchBy(e, pr.BaseRepo.ID, pr.BaseBranch)
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										130
									
								
								models/review.go
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								models/review.go
									
									
									
									
									
								
							@@ -10,7 +10,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"xorm.io/builder"
 | 
						"xorm.io/builder"
 | 
				
			||||||
	"xorm.io/core"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ReviewType defines the sort of feedback a review gives
 | 
					// ReviewType defines the sort of feedback a review gives
 | 
				
			||||||
@@ -53,6 +52,8 @@ type Review struct {
 | 
				
			|||||||
	Issue      *Issue `xorm:"-"`
 | 
						Issue      *Issue `xorm:"-"`
 | 
				
			||||||
	IssueID    int64  `xorm:"index"`
 | 
						IssueID    int64  `xorm:"index"`
 | 
				
			||||||
	Content    string `xorm:"TEXT"`
 | 
						Content    string `xorm:"TEXT"`
 | 
				
			||||||
 | 
						// Official is a review made by an assigned approver (counts towards approval)
 | 
				
			||||||
 | 
						Official bool `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
						CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
				
			||||||
	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
						UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
				
			||||||
@@ -122,23 +123,6 @@ func GetReviewByID(id int64) (*Review, error) {
 | 
				
			|||||||
	return getReviewByID(x, id)
 | 
						return getReviewByID(x, id)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getUniqueApprovalsByPullRequestID(e Engine, prID int64) (reviews []*Review, err error) {
 | 
					 | 
				
			||||||
	reviews = make([]*Review, 0)
 | 
					 | 
				
			||||||
	if err := e.
 | 
					 | 
				
			||||||
		Where("issue_id = ? AND type = ?", prID, ReviewTypeApprove).
 | 
					 | 
				
			||||||
		OrderBy("updated_unix").
 | 
					 | 
				
			||||||
		GroupBy("reviewer_id").
 | 
					 | 
				
			||||||
		Find(&reviews); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetUniqueApprovalsByPullRequestID returns all reviews submitted for a specific pull request
 | 
					 | 
				
			||||||
func GetUniqueApprovalsByPullRequestID(prID int64) ([]*Review, error) {
 | 
					 | 
				
			||||||
	return getUniqueApprovalsByPullRequestID(x, prID)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FindReviewOptions represent possible filters to find reviews
 | 
					// FindReviewOptions represent possible filters to find reviews
 | 
				
			||||||
type FindReviewOptions struct {
 | 
					type FindReviewOptions struct {
 | 
				
			||||||
	Type       ReviewType
 | 
						Type       ReviewType
 | 
				
			||||||
@@ -180,6 +164,27 @@ type CreateReviewOptions struct {
 | 
				
			|||||||
	Type     ReviewType
 | 
						Type     ReviewType
 | 
				
			||||||
	Issue    *Issue
 | 
						Issue    *Issue
 | 
				
			||||||
	Reviewer *User
 | 
						Reviewer *User
 | 
				
			||||||
 | 
						Official bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsOfficialReviewer check if reviewer can make official reviews in issue (counts towards required approvals)
 | 
				
			||||||
 | 
					func IsOfficialReviewer(issue *Issue, reviewer *User) (bool, error) {
 | 
				
			||||||
 | 
						return isOfficialReviewer(x, issue, reviewer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isOfficialReviewer(e Engine, issue *Issue, reviewer *User) (bool, error) {
 | 
				
			||||||
 | 
						pr, err := getPullRequestByIssueID(e, issue.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err = pr.loadProtectedBranch(e); err != nil {
 | 
				
			||||||
 | 
							return false, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pr.ProtectedBranch == nil {
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return pr.ProtectedBranch.isUserOfficialReviewer(e, reviewer)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
 | 
					func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
 | 
				
			||||||
@@ -190,6 +195,7 @@ func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
 | 
				
			|||||||
		Reviewer:   opts.Reviewer,
 | 
							Reviewer:   opts.Reviewer,
 | 
				
			||||||
		ReviewerID: opts.Reviewer.ID,
 | 
							ReviewerID: opts.Reviewer.ID,
 | 
				
			||||||
		Content:    opts.Content,
 | 
							Content:    opts.Content,
 | 
				
			||||||
 | 
							Official:   opts.Official,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if _, err := e.Insert(review); err != nil {
 | 
						if _, err := e.Insert(review); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
@@ -255,6 +261,8 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
 | 
				
			|||||||
		return nil, nil, err
 | 
							return nil, nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var official = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	review, err := getCurrentReview(sess, doer, issue)
 | 
						review, err := getCurrentReview(sess, doer, issue)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if !IsErrReviewNotExist(err) {
 | 
							if !IsErrReviewNotExist(err) {
 | 
				
			||||||
@@ -265,12 +273,24 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
 | 
				
			|||||||
			return nil, nil, ContentEmptyErr{}
 | 
								return nil, nil, ContentEmptyErr{}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
 | 
				
			||||||
 | 
								// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
 | 
				
			||||||
 | 
								if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
 | 
				
			||||||
 | 
									return nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								official, err = isOfficialReviewer(sess, issue, doer)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// No current review. Create a new one!
 | 
							// No current review. Create a new one!
 | 
				
			||||||
		review, err = createReview(sess, CreateReviewOptions{
 | 
							review, err = createReview(sess, CreateReviewOptions{
 | 
				
			||||||
			Type:     reviewType,
 | 
								Type:     reviewType,
 | 
				
			||||||
			Issue:    issue,
 | 
								Issue:    issue,
 | 
				
			||||||
			Reviewer: doer,
 | 
								Reviewer: doer,
 | 
				
			||||||
			Content:  content,
 | 
								Content:  content,
 | 
				
			||||||
 | 
								Official: official,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
@@ -283,10 +303,23 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
 | 
				
			|||||||
			return nil, nil, ContentEmptyErr{}
 | 
								return nil, nil, ContentEmptyErr{}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
 | 
				
			||||||
 | 
								// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
 | 
				
			||||||
 | 
								if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, doer.ID); err != nil {
 | 
				
			||||||
 | 
									return nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								official, err = isOfficialReviewer(sess, issue, doer)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							review.Official = official
 | 
				
			||||||
		review.Issue = issue
 | 
							review.Issue = issue
 | 
				
			||||||
		review.Content = content
 | 
							review.Content = content
 | 
				
			||||||
		review.Type = reviewType
 | 
							review.Type = reviewType
 | 
				
			||||||
		if _, err := sess.ID(review.ID).Cols("content, type").Update(review); err != nil {
 | 
					
 | 
				
			||||||
 | 
							if _, err := sess.ID(review.ID).Cols("content, type, official").Update(review); err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -307,46 +340,33 @@ func SubmitReview(doer *User, issue *Issue, reviewType ReviewType, content strin
 | 
				
			|||||||
	return review, comm, sess.Commit()
 | 
						return review, comm, sess.Commit()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PullReviewersWithType represents the type used to display a review overview
 | 
					// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
 | 
				
			||||||
type PullReviewersWithType struct {
 | 
					func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) {
 | 
				
			||||||
	User              `xorm:"extends"`
 | 
						reviewsUnfiltered := []*Review{}
 | 
				
			||||||
	Type              ReviewType
 | 
					
 | 
				
			||||||
	ReviewUpdatedUnix timeutil.TimeStamp `xorm:"review_updated_unix"`
 | 
						sess := x.NewSession()
 | 
				
			||||||
 | 
						defer sess.Close()
 | 
				
			||||||
 | 
						if err := sess.Begin(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetReviewersByPullID gets all reviewers for a pull request with the statuses
 | 
						// Get latest review of each reviwer, sorted in order they were made
 | 
				
			||||||
func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) {
 | 
						if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND type in (?, ?) GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
 | 
				
			||||||
	irs := []*PullReviewersWithType{}
 | 
							issueID, ReviewTypeApprove, ReviewTypeReject).
 | 
				
			||||||
	if x.Dialect().DBType() == core.MSSQL {
 | 
							Find(&reviewsUnfiltered); err != nil {
 | 
				
			||||||
		err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM
 | 
							return nil, err
 | 
				
			||||||
(SELECT review.id, review.type, review.reviewer_id, max(review.updated_unix) as review_updated_unix
 | 
						}
 | 
				
			||||||
FROM review WHERE review.issue_id=? AND (review.type = ? OR review.type = ?)
 | 
					
 | 
				
			||||||
GROUP BY review.id, review.type, review.reviewer_id) as review
 | 
						// Load reviewer and skip if user is deleted
 | 
				
			||||||
INNER JOIN [user] ON review.reviewer_id = [user].id ORDER BY review_updated_unix DESC`,
 | 
						for _, review := range reviewsUnfiltered {
 | 
				
			||||||
			pullID, ReviewTypeApprove, ReviewTypeReject).
 | 
							if err := review.loadReviewer(sess); err != nil {
 | 
				
			||||||
			Find(&irs)
 | 
								if !IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
									return nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
		err = x.Select("`user`.*, review.type, max(review.updated_unix) as review_updated_unix").
 | 
								reviews = append(reviews, review)
 | 
				
			||||||
			Table("review").
 | 
					 | 
				
			||||||
			Join("INNER", "`user`", "review.reviewer_id = `user`.id").
 | 
					 | 
				
			||||||
			Where("review.issue_id = ? AND (review.type = ? OR review.type = ?)",
 | 
					 | 
				
			||||||
				pullID, ReviewTypeApprove, ReviewTypeReject).
 | 
					 | 
				
			||||||
			GroupBy("`user`.id, review.type").
 | 
					 | 
				
			||||||
			OrderBy("review_updated_unix DESC").
 | 
					 | 
				
			||||||
			Find(&irs)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// We need to group our results by user id _and_ review type, otherwise the query fails when using postgresql.
 | 
					 | 
				
			||||||
	// But becaus we're doing this, we need to manually filter out multiple reviews of different types by the
 | 
					 | 
				
			||||||
	// same person because we only want to show the newest review grouped by user. Thats why we're using a map here.
 | 
					 | 
				
			||||||
	issueReviewers = []*PullReviewersWithType{}
 | 
					 | 
				
			||||||
	usersInArray := make(map[int64]bool)
 | 
					 | 
				
			||||||
	for _, ir := range irs {
 | 
					 | 
				
			||||||
		if !usersInArray[ir.ID] {
 | 
					 | 
				
			||||||
			issueReviewers = append(issueReviewers, ir)
 | 
					 | 
				
			||||||
			usersInArray[ir.ID] = true
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return
 | 
						return reviews, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -98,7 +98,7 @@ func TestCreateReview(t *testing.T) {
 | 
				
			|||||||
	AssertExistsAndLoadBean(t, &Review{Content: "New Review"})
 | 
						AssertExistsAndLoadBean(t, &Review{Content: "New Review"})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestGetReviewersByPullID(t *testing.T) {
 | 
					func TestGetReviewersByIssueID(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, PrepareTestDatabase())
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	issue := AssertExistsAndLoadBean(t, &Issue{ID: 3}).(*Issue)
 | 
						issue := AssertExistsAndLoadBean(t, &Issue{ID: 3}).(*Issue)
 | 
				
			||||||
@@ -106,24 +106,29 @@ func TestGetReviewersByPullID(t *testing.T) {
 | 
				
			|||||||
	user3 := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
 | 
						user3 := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
 | 
				
			||||||
	user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
 | 
						user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expectedReviews := []*PullReviewersWithType{}
 | 
						expectedReviews := []*Review{}
 | 
				
			||||||
	expectedReviews = append(expectedReviews, &PullReviewersWithType{
 | 
						expectedReviews = append(expectedReviews,
 | 
				
			||||||
		User:              *user2,
 | 
							&Review{
 | 
				
			||||||
 | 
								Reviewer:    user3,
 | 
				
			||||||
			Type:        ReviewTypeReject,
 | 
								Type:        ReviewTypeReject,
 | 
				
			||||||
		ReviewUpdatedUnix: 946684810,
 | 
								UpdatedUnix: 946684812,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		&PullReviewersWithType{
 | 
							&Review{
 | 
				
			||||||
			User:              *user3,
 | 
								Reviewer:    user4,
 | 
				
			||||||
			Type:              ReviewTypeReject,
 | 
					 | 
				
			||||||
			ReviewUpdatedUnix: 946684810,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		&PullReviewersWithType{
 | 
					 | 
				
			||||||
			User:              *user4,
 | 
					 | 
				
			||||||
			Type:        ReviewTypeApprove,
 | 
								Type:        ReviewTypeApprove,
 | 
				
			||||||
			ReviewUpdatedUnix: 946684810,
 | 
								UpdatedUnix: 946684813,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&Review{
 | 
				
			||||||
 | 
								Reviewer:    user2,
 | 
				
			||||||
 | 
								Type:        ReviewTypeReject,
 | 
				
			||||||
 | 
								UpdatedUnix: 946684814,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	allReviews, err := GetReviewersByPullID(issue.ID)
 | 
						allReviews, err := GetReviewersByIssueID(issue.ID)
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
	assert.Equal(t, expectedReviews, allReviews)
 | 
						for i, review := range allReviews {
 | 
				
			||||||
 | 
							assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
 | 
				
			||||||
 | 
							assert.Equal(t, expectedReviews[i].Type, review.Type)
 | 
				
			||||||
 | 
							assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,7 +158,7 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
 | 
				
			|||||||
// ProtectBranchForm form for changing protected branch settings
 | 
					// ProtectBranchForm form for changing protected branch settings
 | 
				
			||||||
type ProtectBranchForm struct {
 | 
					type ProtectBranchForm struct {
 | 
				
			||||||
	Protected                bool
 | 
						Protected                bool
 | 
				
			||||||
	EnableWhitelist         bool
 | 
						EnablePush               string
 | 
				
			||||||
	WhitelistUsers           string
 | 
						WhitelistUsers           string
 | 
				
			||||||
	WhitelistTeams           string
 | 
						WhitelistTeams           string
 | 
				
			||||||
	WhitelistDeployKeys      bool
 | 
						WhitelistDeployKeys      bool
 | 
				
			||||||
@@ -168,6 +168,7 @@ type ProtectBranchForm struct {
 | 
				
			|||||||
	EnableStatusCheck        bool `xorm:"NOT NULL DEFAULT false"`
 | 
						EnableStatusCheck        bool `xorm:"NOT NULL DEFAULT false"`
 | 
				
			||||||
	StatusCheckContexts      []string
 | 
						StatusCheckContexts      []string
 | 
				
			||||||
	RequiredApprovals        int64
 | 
						RequiredApprovals        int64
 | 
				
			||||||
 | 
						EnableApprovalsWhitelist bool
 | 
				
			||||||
	ApprovalsWhitelistUsers  string
 | 
						ApprovalsWhitelistUsers  string
 | 
				
			||||||
	ApprovalsWhitelistTeams  string
 | 
						ApprovalsWhitelistTeams  string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1382,9 +1382,13 @@ settings.protected_branch_can_push_yes = You can push
 | 
				
			|||||||
settings.protected_branch_can_push_no = You can not push
 | 
					settings.protected_branch_can_push_no = You can not push
 | 
				
			||||||
settings.branch_protection = Branch Protection for Branch '<b>%s</b>'
 | 
					settings.branch_protection = Branch Protection for Branch '<b>%s</b>'
 | 
				
			||||||
settings.protect_this_branch = Enable Branch Protection
 | 
					settings.protect_this_branch = Enable Branch Protection
 | 
				
			||||||
settings.protect_this_branch_desc = Prevent deletion and disable any Git pushing to the branch.
 | 
					settings.protect_this_branch_desc = Prevents deletion and restricts Git pushing and merging to the branch.
 | 
				
			||||||
settings.protect_whitelist_committers = Enable Push Whitelist
 | 
					settings.protect_disable_push = Disable Push
 | 
				
			||||||
settings.protect_whitelist_committers_desc = Allow whitelisted users or teams to push to this branch (but not force push).
 | 
					settings.protect_disable_push_desc = No pushing will be allowed to this branch.
 | 
				
			||||||
 | 
					settings.protect_enable_push = Enable Push
 | 
				
			||||||
 | 
					settings.protect_enable_push_desc = Anyone with write access will be allowed to push to this branch (but not force push).
 | 
				
			||||||
 | 
					settings.protect_whitelist_committers = Whitelist Restricted Push
 | 
				
			||||||
 | 
					settings.protect_whitelist_committers_desc = Only whitelisted users or teams will be allowed to push to this branch (but not force push).
 | 
				
			||||||
settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push
 | 
					settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push
 | 
				
			||||||
settings.protect_whitelist_users = Whitelisted users for pushing:
 | 
					settings.protect_whitelist_users = Whitelisted users for pushing:
 | 
				
			||||||
settings.protect_whitelist_search_users = Search users…
 | 
					settings.protect_whitelist_search_users = Search users…
 | 
				
			||||||
@@ -1398,7 +1402,9 @@ settings.protect_check_status_contexts = Enable Status Check
 | 
				
			|||||||
settings.protect_check_status_contexts_desc = Require status checks to pass before merging Choose which status checks must pass before branches can be merged into a branch that matches this rule. When enabled, commits must first be pushed to another branch, then merged or pushed directly to a branch that matches this rule after status checks have passed. If no contexts are selected, the last commit must be successful regardless of context.
 | 
					settings.protect_check_status_contexts_desc = Require status checks to pass before merging Choose which status checks must pass before branches can be merged into a branch that matches this rule. When enabled, commits must first be pushed to another branch, then merged or pushed directly to a branch that matches this rule after status checks have passed. If no contexts are selected, the last commit must be successful regardless of context.
 | 
				
			||||||
settings.protect_check_status_contexts_list = Status checks found in the last week for this repository
 | 
					settings.protect_check_status_contexts_list = Status checks found in the last week for this repository
 | 
				
			||||||
settings.protect_required_approvals = Required approvals:
 | 
					settings.protect_required_approvals = Required approvals:
 | 
				
			||||||
settings.protect_required_approvals_desc = Allow only to merge pull request with enough positive reviews of whitelisted users or teams.
 | 
					settings.protect_required_approvals_desc = Allow only to merge pull request with enough positive reviews.
 | 
				
			||||||
 | 
					settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted users or teams
 | 
				
			||||||
 | 
					settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals. 
 | 
				
			||||||
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
 | 
					settings.protect_approvals_whitelist_users = Whitelisted reviewers:
 | 
				
			||||||
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
 | 
					settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
 | 
				
			||||||
settings.add_protected_branch = Enable protection
 | 
					settings.add_protected_branch = Enable protection
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -98,7 +98,7 @@ func HookPreReceive(ctx *macaron.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		canPush := false
 | 
							canPush := false
 | 
				
			||||||
		if isDeployKey {
 | 
							if isDeployKey {
 | 
				
			||||||
			canPush = protectBranch.WhitelistDeployKeys
 | 
								canPush = protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			canPush = protectBranch.CanUserPush(userID)
 | 
								canPush = protectBranch.CanUserPush(userID)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -935,9 +935,9 @@ func ViewIssue(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
 | 
							ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.Data["PullReviewersWithType"], err = models.GetReviewersByPullID(issue.ID)
 | 
							ctx.Data["PullReviewers"], err = models.GetReviewersByIssueID(issue.ID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ctx.ServerError("GetReviewersByPullID", err)
 | 
								ctx.ServerError("GetReviewersByIssueID", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -196,32 +196,55 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
 | 
							var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
 | 
				
			||||||
		protectBranch.EnableWhitelist = f.EnableWhitelist
 | 
							switch f.EnablePush {
 | 
				
			||||||
 | 
							case "all":
 | 
				
			||||||
 | 
								protectBranch.CanPush = true
 | 
				
			||||||
 | 
								protectBranch.EnableWhitelist = false
 | 
				
			||||||
 | 
								protectBranch.WhitelistDeployKeys = false
 | 
				
			||||||
 | 
							case "whitelist":
 | 
				
			||||||
 | 
								protectBranch.CanPush = true
 | 
				
			||||||
 | 
								protectBranch.EnableWhitelist = true
 | 
				
			||||||
 | 
								protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
 | 
				
			||||||
			if strings.TrimSpace(f.WhitelistUsers) != "" {
 | 
								if strings.TrimSpace(f.WhitelistUsers) != "" {
 | 
				
			||||||
				whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
 | 
									whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if strings.TrimSpace(f.WhitelistTeams) != "" {
 | 
								if strings.TrimSpace(f.WhitelistTeams) != "" {
 | 
				
			||||||
				whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
 | 
									whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								protectBranch.CanPush = false
 | 
				
			||||||
 | 
								protectBranch.EnableWhitelist = false
 | 
				
			||||||
 | 
								protectBranch.WhitelistDeployKeys = false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
 | 
							protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
 | 
				
			||||||
 | 
							if f.EnableMergeWhitelist {
 | 
				
			||||||
			if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
 | 
								if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
 | 
				
			||||||
				mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
 | 
									mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
 | 
								if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
 | 
				
			||||||
				mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
 | 
									mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protectBranch.EnableStatusCheck = f.EnableStatusCheck
 | 
							protectBranch.EnableStatusCheck = f.EnableStatusCheck
 | 
				
			||||||
 | 
							if f.EnableStatusCheck {
 | 
				
			||||||
			protectBranch.StatusCheckContexts = f.StatusCheckContexts
 | 
								protectBranch.StatusCheckContexts = f.StatusCheckContexts
 | 
				
			||||||
		protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
 | 
							} else {
 | 
				
			||||||
 | 
								protectBranch.StatusCheckContexts = nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		protectBranch.RequiredApprovals = f.RequiredApprovals
 | 
							protectBranch.RequiredApprovals = f.RequiredApprovals
 | 
				
			||||||
 | 
							protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
 | 
				
			||||||
 | 
							if f.EnableApprovalsWhitelist {
 | 
				
			||||||
			if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
 | 
								if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
 | 
				
			||||||
				approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
 | 
									approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
 | 
								if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
 | 
				
			||||||
				approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
 | 
									approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
 | 
							err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
 | 
				
			||||||
			UserIDs:          whitelistUsers,
 | 
								UserIDs:          whitelistUsers,
 | 
				
			||||||
			TeamIDs:          whitelistTeams,
 | 
								TeamIDs:          whitelistTeams,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,7 @@ func CreateCodeComment(doer *models.User, issue *models.Issue, line int64, conte
 | 
				
			|||||||
			Type:     models.ReviewTypePending,
 | 
								Type:     models.ReviewTypePending,
 | 
				
			||||||
			Reviewer: doer,
 | 
								Reviewer: doer,
 | 
				
			||||||
			Issue:    issue,
 | 
								Issue:    issue,
 | 
				
			||||||
 | 
								Official: false,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
{{if gt (len .PullReviewersWithType) 0}}
 | 
					{{if gt (len .PullReviewers) 0}}
 | 
				
			||||||
	<div class="comment box">
 | 
						<div class="comment box">
 | 
				
			||||||
		<div class="content">
 | 
							<div class="content">
 | 
				
			||||||
			<div class="ui segment">
 | 
								<div class="ui segment">
 | 
				
			||||||
				<h4>{{$.i18n.Tr "repo.issues.review.reviewers"}}</h4>
 | 
									<h4>{{$.i18n.Tr "repo.issues.review.reviewers"}}</h4>
 | 
				
			||||||
				{{range .PullReviewersWithType}}
 | 
									{{range .PullReviewers}}
 | 
				
			||||||
					{{ $createdStr:= TimeSinceUnix .ReviewUpdatedUnix $.Lang }}
 | 
										{{ $createdStr:= TimeSinceUnix .UpdatedUnix $.Lang }}
 | 
				
			||||||
					<div class="ui divider"></div>
 | 
										<div class="ui divider"></div>
 | 
				
			||||||
					<div class="review-item">
 | 
										<div class="review-item">
 | 
				
			||||||
						<span class="type-icon text {{if eq .Type 1}}green
 | 
											<span class="type-icon text {{if eq .Type 1}}green
 | 
				
			||||||
@@ -13,10 +13,10 @@
 | 
				
			|||||||
							{{else}}grey{{end}}">
 | 
												{{else}}grey{{end}}">
 | 
				
			||||||
							<span class="octicon octicon-{{.Type.Icon}}"></span>
 | 
												<span class="octicon octicon-{{.Type.Icon}}"></span>
 | 
				
			||||||
						</span>
 | 
											</span>
 | 
				
			||||||
						<a class="ui avatar image" href="{{.HomeLink}}">
 | 
											<a class="ui avatar image" href="{{.Reviewer.HomeLink}}">
 | 
				
			||||||
							<img src="{{.RelAvatarLink}}">
 | 
												<img src="{{.Reviewer.RelAvatarLink}}">
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
						<span class="text grey"><a href="{{.HomeLink}}">{{.Name}}</a>
 | 
											<span class="text grey"><a href="{{.Reviewer.HomeLink}}">{{.Reviewer.Name}}</a>
 | 
				
			||||||
							{{if eq .Type 1}}
 | 
												{{if eq .Type 1}}
 | 
				
			||||||
								{{$.i18n.Tr "repo.issues.review.approve" $createdStr | Safe}}
 | 
													{{$.i18n.Tr "repo.issues.review.approve" $createdStr | Safe}}
 | 
				
			||||||
							{{else if eq .Type 2}}
 | 
												{{else if eq .Type 2}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,8 +19,22 @@
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div id="protection_box" class="fields {{if not .Branch.IsProtected}}disabled{{end}}">
 | 
									<div id="protection_box" class="fields {{if not .Branch.IsProtected}}disabled{{end}}">
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<div class="ui checkbox">
 | 
											<div class="ui radio checkbox">
 | 
				
			||||||
							<input class="enable-whitelist" name="enable_whitelist" type="checkbox" data-target="#whitelist_box" {{if .Branch.EnableWhitelist}}checked{{end}}>
 | 
												<input name="enable_push" type="radio" value="none" class="disable-whitelist" data-target="#whitelist_box" {{if not .Branch.CanPush}}checked{{end}}>
 | 
				
			||||||
 | 
												<label>{{.i18n.Tr "repo.settings.protect_disable_push"}}</label>
 | 
				
			||||||
 | 
												<p class="help">{{.i18n.Tr "repo.settings.protect_disable_push_desc"}}</p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="field">
 | 
				
			||||||
 | 
											<div class="ui radio checkbox">
 | 
				
			||||||
 | 
												<input name="enable_push" type="radio" value="all" class="disable-whitelist" data-target="#whitelist_box" {{if and (.Branch.CanPush) (not .Branch.EnableWhitelist)}}checked{{end}}>
 | 
				
			||||||
 | 
												<label>{{.i18n.Tr "repo.settings.protect_enable_push"}}</label>
 | 
				
			||||||
 | 
												<p class="help">{{.i18n.Tr "repo.settings.protect_enable_push_desc"}}</p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="field">
 | 
				
			||||||
 | 
											<div class="ui radio checkbox">
 | 
				
			||||||
 | 
												<input name="enable_push" type="radio" value="whitelist" class="enable-whitelist" data-target="#whitelist_box" {{if and (.Branch.CanPush) (.Branch.EnableWhitelist)}}checked{{end}}>
 | 
				
			||||||
							<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label>
 | 
												<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label>
 | 
				
			||||||
							<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
 | 
												<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
@@ -148,7 +162,14 @@
 | 
				
			|||||||
						<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}">
 | 
											<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}">
 | 
				
			||||||
						<p class="help">{{.i18n.Tr "repo.settings.protect_required_approvals_desc"}}</p>
 | 
											<p class="help">{{.i18n.Tr "repo.settings.protect_required_approvals_desc"}}</p>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<div class="fields">
 | 
										<div class="field">
 | 
				
			||||||
 | 
											<div class="ui checkbox">
 | 
				
			||||||
 | 
												<input class="enable-whitelist" name="enable_approvals_whitelist" type="checkbox" data-target="#approvals_whitelist_box" {{if .Branch.EnableApprovalsWhitelist}}checked{{end}}>
 | 
				
			||||||
 | 
												<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_enabled"}}</label>
 | 
				
			||||||
 | 
												<p class="help">{{.i18n.Tr "repo.settings.protect_approvals_whitelist_enabled_desc"}}</p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div id="approvals_whitelist_box" class="fields {{if not .Branch.EnableApprovalsWhitelist}}disabled{{end}}">
 | 
				
			||||||
						<div class="whitelist field">
 | 
											<div class="whitelist field">
 | 
				
			||||||
							<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
 | 
												<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
 | 
				
			||||||
							<div class="ui multiple search selection dropdown">
 | 
												<div class="ui multiple search selection dropdown">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1043,6 +1043,11 @@ function initRepository() {
 | 
				
			|||||||
        $($(this).data('target')).addClass('disabled');
 | 
					        $($(this).data('target')).addClass('disabled');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    $('.disable-whitelist').change(function () {
 | 
				
			||||||
 | 
					      if (this.checked) {
 | 
				
			||||||
 | 
					        $($(this).data('target')).addClass('disabled');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user