* 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.tags/v1.11.0-rc1
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), | ||||
}) | }) |
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"` | ||||
// 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 | ||||
} | } | ||||
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 { | ||||
// 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) | |||||
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 | |||||
return approvals | |||||
} | } | ||||
// GetProtectedBranchByRepoID getting protected branch by repo ID | // GetProtectedBranchByRepoID getting protected branch by repo ID | ||||
// 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 | ||||
} | } |
reviewer_id: 2 | reviewer_id: 2 | ||||
issue_id: 3 | issue_id: 3 | ||||
content: "New review 3" | content: "New review 3" | ||||
updated_unix: 946684810 | |||||
created_unix: 946684810 | |||||
updated_unix: 946684811 | |||||
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 | |||||
created_unix: 946684810 | |||||
updated_unix: 946684812 | |||||
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 | |||||
created_unix: 946684810 | |||||
updated_unix: 946684813 | |||||
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 | |||||
created_unix: 946684810 | |||||
updated_unix: 946684814 | |||||
created_unix: 946684814 | |||||
- | - | ||||
id: 10 | id: 10 | ||||
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 | |||||
created_unix: 946684810 | |||||
updated_unix: 946684815 | |||||
created_unix: 946684815 |
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 |
// 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() | |||||
} |
// 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 |
// 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 | ||||
} | } | ||||
"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 | ||||
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"` | ||||
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 | ||||
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) { | ||||
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 | ||||
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) { | ||||
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 | ||||
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 | ||||
} | } | ||||
} | } | ||||
return review, comm, sess.Commit() | return review, comm, sess.Commit() | ||||
} | } | ||||
// PullReviewersWithType represents the type used to display a review overview | |||||
type PullReviewersWithType struct { | |||||
User `xorm:"extends"` | |||||
Type ReviewType | |||||
ReviewUpdatedUnix timeutil.TimeStamp `xorm:"review_updated_unix"` | |||||
} | |||||
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request | |||||
func GetReviewersByIssueID(issueID int64) (reviews []*Review, err error) { | |||||
reviewsUnfiltered := []*Review{} | |||||
// GetReviewersByPullID gets all reviewers for a pull request with the statuses | |||||
func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) { | |||||
irs := []*PullReviewersWithType{} | |||||
if x.Dialect().DBType() == core.MSSQL { | |||||
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) | |||||
sess := x.NewSession() | |||||
defer sess.Close() | |||||
if err := sess.Begin(); err != nil { | |||||
return nil, err | |||||
} | } | ||||
// 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 | |||||
// Get latest review of each reviwer, sorted in order they were made | |||||
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", | |||||
issueID, ReviewTypeApprove, ReviewTypeReject). | |||||
Find(&reviewsUnfiltered); err != nil { | |||||
return nil, err | |||||
} | |||||
// Load reviewer and skip if user is deleted | |||||
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 | |||||
} | } |
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) | ||||
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 = append(expectedReviews, &PullReviewersWithType{ | |||||
User: *user2, | |||||
Type: ReviewTypeReject, | |||||
ReviewUpdatedUnix: 946684810, | |||||
}, | |||||
&PullReviewersWithType{ | |||||
User: *user3, | |||||
Type: ReviewTypeReject, | |||||
ReviewUpdatedUnix: 946684810, | |||||
expectedReviews := []*Review{} | |||||
expectedReviews = append(expectedReviews, | |||||
&Review{ | |||||
Reviewer: user3, | |||||
Type: ReviewTypeReject, | |||||
UpdatedUnix: 946684812, | |||||
}, | }, | ||||
&PullReviewersWithType{ | |||||
User: *user4, | |||||
Type: ReviewTypeApprove, | |||||
ReviewUpdatedUnix: 946684810, | |||||
&Review{ | |||||
Reviewer: user4, | |||||
Type: ReviewTypeApprove, | |||||
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) | |||||
} | |||||
} | } |
// ProtectBranchForm form for changing protected branch settings | // ProtectBranchForm form for changing protected branch settings | ||||
type ProtectBranchForm struct { | type ProtectBranchForm struct { | ||||
Protected bool | |||||
EnableWhitelist bool | |||||
WhitelistUsers string | |||||
WhitelistTeams string | |||||
WhitelistDeployKeys bool | |||||
EnableMergeWhitelist bool | |||||
MergeWhitelistUsers string | |||||
MergeWhitelistTeams string | |||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` | |||||
StatusCheckContexts []string | |||||
RequiredApprovals int64 | |||||
ApprovalsWhitelistUsers string | |||||
ApprovalsWhitelistTeams string | |||||
Protected bool | |||||
EnablePush string | |||||
WhitelistUsers string | |||||
WhitelistTeams string | |||||
WhitelistDeployKeys bool | |||||
EnableMergeWhitelist bool | |||||
MergeWhitelistUsers string | |||||
MergeWhitelistTeams string | |||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` | |||||
StatusCheckContexts []string | |||||
RequiredApprovals int64 | |||||
EnableApprovalsWhitelist bool | |||||
ApprovalsWhitelistUsers string | |||||
ApprovalsWhitelistTeams string | |||||
} | } | ||||
// Validate validates the fields | // Validate validates the fields |
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_whitelist_committers = Enable Push Whitelist | |||||
settings.protect_whitelist_committers_desc = Allow whitelisted users or teams to push to this branch (but not force push). | |||||
settings.protect_this_branch_desc = Prevents deletion and restricts Git pushing and merging to the branch. | |||||
settings.protect_disable_push = Disable 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… | ||||
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 |
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) | ||||
} | } |
} | } | ||||
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 | ||||
} | } | ||||
} | } |
} | } | ||||
var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 | var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 | ||||
protectBranch.EnableWhitelist = f.EnableWhitelist | |||||
if strings.TrimSpace(f.WhitelistUsers) != "" { | |||||
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) | |||||
} | |||||
if strings.TrimSpace(f.WhitelistTeams) != "" { | |||||
whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) | |||||
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) != "" { | |||||
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) != "" { | |||||
mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) | |||||
} | |||||
if strings.TrimSpace(f.MergeWhitelistTeams) != "" { | |||||
mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) | |||||
if f.EnableMergeWhitelist { | |||||
if strings.TrimSpace(f.MergeWhitelistUsers) != "" { | |||||
mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) | |||||
} | |||||
if strings.TrimSpace(f.MergeWhitelistTeams) != "" { | |||||
mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) | |||||
} | |||||
} | } | ||||
protectBranch.EnableStatusCheck = f.EnableStatusCheck | protectBranch.EnableStatusCheck = f.EnableStatusCheck | ||||
protectBranch.StatusCheckContexts = f.StatusCheckContexts | |||||
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys | |||||
if f.EnableStatusCheck { | |||||
protectBranch.StatusCheckContexts = f.StatusCheckContexts | |||||
} else { | |||||
protectBranch.StatusCheckContexts = nil | |||||
} | |||||
protectBranch.RequiredApprovals = f.RequiredApprovals | protectBranch.RequiredApprovals = f.RequiredApprovals | ||||
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { | |||||
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) | |||||
} | |||||
if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" { | |||||
approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ",")) | |||||
protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist | |||||
if f.EnableApprovalsWhitelist { | |||||
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { | |||||
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) | |||||
} | |||||
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, |
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 |
{{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}} | |||||
{{ $createdStr:= TimeSinceUnix .ReviewUpdatedUnix $.Lang }} | |||||
{{range .PullReviewers}} | |||||
{{ $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 | ||||
{{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}}"> | |||||
<img src="{{.RelAvatarLink}}"> | |||||
<a class="ui avatar image" href="{{.Reviewer.HomeLink}}"> | |||||
<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}} |
</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"> | |||||
<input class="enable-whitelist" name="enable_whitelist" type="checkbox" data-target="#whitelist_box" {{if .Branch.EnableWhitelist}}checked{{end}}> | |||||
<div class="ui radio checkbox"> | |||||
<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> | ||||
<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"> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
{{if .Owner.IsOrganization}} | |||||
<br> | |||||
<div class="whitelist field"> | |||||
<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label> | |||||
<div class="ui multiple search selection dropdown"> | |||||
<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="menu"> | |||||
{{range .Teams}} | |||||
<div class="item" data-value="{{.ID}}"> | |||||
<i class="octicon octicon-jersey"></i> | |||||
{{.Name}} | |||||
{{if .Owner.IsOrganization}} | |||||
<br> | |||||
<div class="whitelist field"> | |||||
<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label> | |||||
<div class="ui multiple search selection dropdown"> | |||||
<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="menu"> | |||||
{{range .Teams}} | |||||
<div class="item" data-value="{{.ID}}"> | |||||
<i class="octicon octicon-jersey"></i> | |||||
{{.Name}} | |||||
</div> | |||||
{{end}} | |||||
</div> | </div> | ||||
{{end}} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | |||||
{{end}} | |||||
{{end}} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
$($(this).data('target')).addClass('disabled'); | $($(this).data('target')).addClass('disabled'); | ||||
} | } | ||||
}); | }); | ||||
$('.disable-whitelist').change(function () { | |||||
if (this.checked) { | |||||
$($(this).data('target')).addClass('disabled'); | |||||
} | |||||
}); | |||||
} | } | ||||
} | } | ||||