mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01: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
						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"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetReviewersByPullID gets all reviewers for a pull request with the statuses | 	sess := x.NewSession() | ||||||
| func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) { | 	defer sess.Close() | ||||||
| 	irs := []*PullReviewersWithType{} | 	if err := sess.Begin(); err != nil { | ||||||
| 	if x.Dialect().DBType() == core.MSSQL { | 		return nil, err | ||||||
| 		err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM |  | ||||||
| (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 |  | ||||||
| INNER JOIN [user] ON review.reviewer_id = [user].id ORDER BY review_updated_unix DESC`, |  | ||||||
| 			pullID, ReviewTypeApprove, ReviewTypeReject). |  | ||||||
| 			Find(&irs) |  | ||||||
| 	} else { |  | ||||||
| 		err = x.Select("`user`.*, review.type, max(review.updated_unix) as review_updated_unix"). |  | ||||||
| 			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. | 	// Get latest review of each reviwer, sorted in order they were made | ||||||
| 	// But becaus we're doing this, we need to manually filter out multiple reviews of different types by the | 	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", | ||||||
| 	// same person because we only want to show the newest review grouped by user. Thats why we're using a map here. | 		issueID, ReviewTypeApprove, ReviewTypeReject). | ||||||
| 	issueReviewers = []*PullReviewersWithType{} | 		Find(&reviewsUnfiltered); err != nil { | ||||||
| 	usersInArray := make(map[int64]bool) | 		return nil, err | ||||||
| 	for _, ir := range irs { | 	} | ||||||
| 		if !usersInArray[ir.ID] { |  | ||||||
| 			issueReviewers = append(issueReviewers, ir) | 	// Load reviewer and skip if user is deleted | ||||||
| 			usersInArray[ir.ID] = true | 	for _, review := range reviewsUnfiltered { | ||||||
|  | 		if err := review.loadReviewer(sess); err != nil { | ||||||
|  | 			if !IsErrUserNotExist(err) { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			reviews = append(reviews, review) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	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{ | ||||||
| 		Type:              ReviewTypeReject, | 			Reviewer:    user3, | ||||||
| 		ReviewUpdatedUnix: 946684810, | 			Type:        ReviewTypeReject, | ||||||
| 	}, | 			UpdatedUnix: 946684812, | ||||||
| 		&PullReviewersWithType{ |  | ||||||
| 			User:              *user3, |  | ||||||
| 			Type:              ReviewTypeReject, |  | ||||||
| 			ReviewUpdatedUnix: 946684810, |  | ||||||
| 		}, | 		}, | ||||||
| 		&PullReviewersWithType{ | 		&Review{ | ||||||
| 			User:              *user4, | 			Reviewer:    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) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -157,19 +157,20 @@ 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 | ||||||
| 	EnableMergeWhitelist    bool | 	EnableMergeWhitelist     bool | ||||||
| 	MergeWhitelistUsers     string | 	MergeWhitelistUsers      string | ||||||
| 	MergeWhitelistTeams     string | 	MergeWhitelistTeams      string | ||||||
| 	EnableStatusCheck       bool `xorm:"NOT NULL DEFAULT false"` | 	EnableStatusCheck        bool `xorm:"NOT NULL DEFAULT false"` | ||||||
| 	StatusCheckContexts     []string | 	StatusCheckContexts      []string | ||||||
| 	RequiredApprovals       int64 | 	RequiredApprovals        int64 | ||||||
| 	ApprovalsWhitelistUsers string | 	EnableApprovalsWhitelist bool | ||||||
| 	ApprovalsWhitelistTeams string | 	ApprovalsWhitelistUsers  string | ||||||
|  | 	ApprovalsWhitelistTeams  string | ||||||
| } | } | ||||||
|  |  | ||||||
| // Validate validates the fields | // Validate validates the fields | ||||||
|   | |||||||
| @@ -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 { | ||||||
| 		if strings.TrimSpace(f.WhitelistUsers) != "" { | 		case "all": | ||||||
| 			whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) | 			protectBranch.CanPush = true | ||||||
| 		} | 			protectBranch.EnableWhitelist = false | ||||||
| 		if strings.TrimSpace(f.WhitelistTeams) != "" { | 			protectBranch.WhitelistDeployKeys = false | ||||||
| 			whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) | 		case "whitelist": | ||||||
|  | 			protectBranch.CanPush = true | ||||||
|  | 			protectBranch.EnableWhitelist = true | ||||||
|  | 			protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys | ||||||
|  | 			if strings.TrimSpace(f.WhitelistUsers) != "" { | ||||||
|  | 				whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) | ||||||
|  | 			} | ||||||
|  | 			if strings.TrimSpace(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 strings.TrimSpace(f.MergeWhitelistUsers) != "" { | 		if f.EnableMergeWhitelist { | ||||||
| 			mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) | 			if strings.TrimSpace(f.MergeWhitelistUsers) != "" { | ||||||
| 		} | 				mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) | ||||||
| 		if strings.TrimSpace(f.MergeWhitelistTeams) != "" { | 			} | ||||||
| 			mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) | 			if strings.TrimSpace(f.MergeWhitelistTeams) != "" { | ||||||
|  | 				mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		protectBranch.EnableStatusCheck = f.EnableStatusCheck | 		protectBranch.EnableStatusCheck = f.EnableStatusCheck | ||||||
| 		protectBranch.StatusCheckContexts = f.StatusCheckContexts | 		if f.EnableStatusCheck { | ||||||
| 		protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys | 			protectBranch.StatusCheckContexts = f.StatusCheckContexts | ||||||
|  | 		} else { | ||||||
|  | 			protectBranch.StatusCheckContexts = nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		protectBranch.RequiredApprovals = f.RequiredApprovals | 		protectBranch.RequiredApprovals = f.RequiredApprovals | ||||||
| 		if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { | 		protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist | ||||||
| 			approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) | 		if f.EnableApprovalsWhitelist { | ||||||
| 		} | 			if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { | ||||||
| 		if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" { | 				approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) | ||||||
| 			approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ",")) | 			} | ||||||
|  | 			if strings.TrimSpace(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"> | ||||||
| @@ -164,24 +185,24 @@ | |||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 					{{if .Owner.IsOrganization}} | 						{{if .Owner.IsOrganization}} | ||||||
| 						<br> | 							<br> | ||||||
| 						<div class="whitelist field"> | 							<div class="whitelist field"> | ||||||
| 							<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label> | 								<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label> | ||||||
| 							<div class="ui multiple search selection dropdown"> | 								<div class="ui multiple search selection dropdown"> | ||||||
| 								<input type="hidden" name="approvals_whitelist_teams" value="{{.approvals_whitelist_teams}}"> | 									<input type="hidden" name="approvals_whitelist_teams" value="{{.approvals_whitelist_teams}}"> | ||||||
| 								<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div> | 									<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div> | ||||||
| 								<div class="menu"> | 									<div class="menu"> | ||||||
| 								{{range .Teams}} | 									{{range .Teams}} | ||||||
| 									<div class="item" data-value="{{.ID}}"> | 										<div class="item" data-value="{{.ID}}"> | ||||||
| 										<i class="octicon octicon-jersey"></i> | 											<i class="octicon octicon-jersey"></i> | ||||||
| 									{{.Name}} | 										{{.Name}} | ||||||
|  | 										</div> | ||||||
|  | 									{{end}} | ||||||
| 									</div> | 									</div> | ||||||
| 								{{end}} |  | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						{{end}} | ||||||
| 					{{end}} |  | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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