Approvals at Branch Protection (#5350)

* Add branch protection for approvals

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add required approvals

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add missing comments and fmt

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add type = approval and group by reviewer_id to review

* Prevent users from adding negative review limits

* Add migration for approval whitelists

Signed-off-by: Jonas Franz <info@jonasfranz.software>
This commit is contained in:
Jonas Franz 2018-12-11 12:28:37 +01:00 committed by Lunny Xiao
parent 64680b72bd
commit 9681c83734
13 changed files with 251 additions and 41 deletions

View file

@ -23,18 +23,21 @@ const (
// ProtectedBranch struct // ProtectedBranch struct
type ProtectedBranch struct { type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"` RepoID int64 `xorm:"UNIQUE(s)"`
BranchName string `xorm:"UNIQUE(s)"` BranchName string `xorm:"UNIQUE(s)"`
CanPush bool `xorm:"NOT NULL DEFAULT false"` CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix util.TimeStamp `xorm:"created"` ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
UpdatedUnix util.TimeStamp `xorm:"updated"` ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
CreatedUnix util.TimeStamp `xorm:"created"`
UpdatedUnix util.TimeStamp `xorm:"updated"`
} }
// IsProtected returns if the branch is protected // IsProtected returns if the branch is protected
@ -86,6 +89,41 @@ func (protectBranch *ProtectedBranch) CanUserMerge(userID int64) bool {
return in return in
} }
// HasEnoughApprovals returns true if pr has enough granted approvals.
func (protectBranch *ProtectedBranch) HasEnoughApprovals(pr *PullRequest) bool {
if protectBranch.RequiredApprovals == 0 {
return true
}
return protectBranch.GetGrantedApprovalsCount(pr) >= protectBranch.RequiredApprovals
}
// 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 {
reviews, err := GetReviewersByPullID(pr.ID)
if err != nil {
log.Error(1, "GetUniqueApprovalsByPullRequestID:", err)
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(1, "UsersInTeamsCount:", err)
return 0
}
return approvalTeamCount + approvals
}
// GetProtectedBranchByRepoID getting protected branch by repo ID // GetProtectedBranchByRepoID getting protected branch by repo ID
func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) { func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0) protectedBranches := make([]*ProtectedBranch, 0)
@ -118,40 +156,64 @@ func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
return rel, nil return rel, nil
} }
// WhitelistOptions represent all sorts of whitelists used for protected branches
type WhitelistOptions struct {
UserIDs []int64
TeamIDs []int64
MergeUserIDs []int64
MergeTeamIDs []int64
ApprovalsUserIDs []int64
ApprovalsTeamIDs []int64
}
// UpdateProtectBranch saves branch protection options of repository. // UpdateProtectBranch saves branch protection options of repository.
// If ID is 0, it creates a new record. Otherwise, updates existing record. // If ID is 0, it creates a new record. Otherwise, updates existing record.
// This function also performs check if whitelist user and team's IDs have been changed // This function also performs check if whitelist user and team's IDs have been changed
// to avoid unnecessary whitelist delete and regenerate. // to avoid unnecessary whitelist delete and regenerate.
func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs, mergeWhitelistUserIDs, mergeWhitelistTeamIDs []int64) (err error) { func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
if err = repo.GetOwner(); err != nil { if err = repo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err) return fmt.Errorf("GetOwner: %v", err)
} }
whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, whitelistUserIDs) whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil { if err != nil {
return err return err
} }
protectBranch.WhitelistUserIDs = whitelist protectBranch.WhitelistUserIDs = whitelist
whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, mergeWhitelistUserIDs) whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil { if err != nil {
return err return err
} }
protectBranch.MergeWhitelistUserIDs = whitelist protectBranch.MergeWhitelistUserIDs = whitelist
whitelist, err = updateUserWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist
// if the repo is in an organization // if the repo is in an organization
whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, whitelistTeamIDs) whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil { if err != nil {
return err return err
} }
protectBranch.WhitelistTeamIDs = whitelist protectBranch.WhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, mergeWhitelistTeamIDs) whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil { if err != nil {
return err return err
} }
protectBranch.MergeWhitelistTeamIDs = whitelist protectBranch.MergeWhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists // Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 { if protectBranch.ID == 0 {
if _, err = x.Insert(protectBranch); err != nil { if _, err = x.Insert(protectBranch); err != nil {
@ -213,7 +275,7 @@ func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User)
} }
// IsProtectedBranchForMerging checks if branch is protected for merging // IsProtectedBranchForMerging checks if branch is protected for merging
func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *User) (bool, error) { func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName string, doer *User) (bool, error) {
if doer == nil { if doer == nil {
return true, nil return true, nil
} }
@ -227,7 +289,7 @@ func (repo *Repository) IsProtectedBranchForMerging(branchName string, doer *Use
if err != nil { if err != nil {
return true, err return true, err
} else if has { } else if has {
return !protectedBranch.CanUserMerge(doer.ID), nil return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr), nil
} }
return false, nil return false, nil
@ -270,14 +332,14 @@ func updateTeamWhitelist(repo *Repository, currentWhitelist, newWhitelist []int6
return currentWhitelist, nil return currentWhitelist, nil
} }
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite) teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeRead)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
} }
whitelist = make([]int64, 0, len(teams)) whitelist = make([]int64, 0, len(teams))
for i := range teams { for i := range teams {
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(newWhitelist, teams[i].ID) { if com.IsSliceContainsInt64(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID) whitelist = append(whitelist, teams[i].ID)
} }
} }

View file

@ -200,6 +200,8 @@ var migrations = []Migration{
NewMigration("add review", addReview), NewMigration("add review", addReview),
// v73 -> v74 // v73 -> v74
NewMigration("add must_change_password column for users table", addMustChangePassword), NewMigration("add must_change_password column for users table", addMustChangePassword),
// v74 -> v75
NewMigration("add approval whitelists to protected branches", addApprovalWhitelistsToProtectedBranches),
} }
// Migrate database to current version // Migrate database to current version

16
models/migrations/v74.go Normal file
View file

@ -0,0 +1,16 @@
// Copyright 2018 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 "github.com/go-xorm/xorm"
func addApprovalWhitelistsToProtectedBranches(x *xorm.Engine) error {
type ProtectedBranch struct {
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync2(new(ProtectedBranch))
}

View file

@ -700,6 +700,14 @@ func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser)) return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
} }
// UsersInTeamsCount counts the number of users which are in userIDs and teamIDs
func UsersInTeamsCount(userIDs []int64, teamIDs []int64) (count int64, err error) {
if count, err = x.In("uid", userIDs).In("team_id", teamIDs).Count(new(TeamUser)); err != nil {
return 0, err
}
return
}
// ___________ __________ // ___________ __________
// \__ ___/___ _____ _____\______ \ ____ ______ ____ // \__ ___/___ _____ _____\______ \ ____ ______ ____
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \ // | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \

View file

@ -346,3 +346,17 @@ func TestHasTeamRepo(t *testing.T) {
test(2, 3, true) test(2, 3, true)
test(2, 5, false) test(2, 5, false)
} }
func TestUsersInTeamsCount(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
test := func(teamIDs []int64, userIDs []int64, expected int64) {
count, err := UsersInTeamsCount(teamIDs, userIDs)
assert.NoError(t, err)
assert.Equal(t, expected, count)
}
test([]int64{2}, []int64{1, 2, 3, 4}, 2)
test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2)
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3)
}

View file

@ -60,14 +60,15 @@ type PullRequest struct {
Issue *Issue `xorm:"-"` Issue *Issue `xorm:"-"`
Index int64 Index int64
HeadRepoID int64 `xorm:"INDEX"` HeadRepoID int64 `xorm:"INDEX"`
HeadRepo *Repository `xorm:"-"` HeadRepo *Repository `xorm:"-"`
BaseRepoID int64 `xorm:"INDEX"` BaseRepoID int64 `xorm:"INDEX"`
BaseRepo *Repository `xorm:"-"` BaseRepo *Repository `xorm:"-"`
HeadUserName string HeadUserName string
HeadBranch string HeadBranch string
BaseBranch string BaseBranch string
MergeBase string `xorm:"VARCHAR(40)"` ProtectedBranch *ProtectedBranch `xorm:"-"`
MergeBase string `xorm:"VARCHAR(40)"`
HasMerged bool `xorm:"INDEX"` HasMerged bool `xorm:"INDEX"`
MergedCommitID string `xorm:"VARCHAR(40)"` MergedCommitID string `xorm:"VARCHAR(40)"`
@ -110,6 +111,12 @@ func (pr *PullRequest) loadIssue(e Engine) (err error) {
return err return err
} }
// LoadProtectedBranch loads the protected branch of the base branch
func (pr *PullRequest) LoadProtectedBranch() (err error) {
pr.ProtectedBranch, err = GetProtectedBranchBy(pr.BaseRepo.ID, pr.BaseBranch)
return
}
// GetDefaultMergeMessage returns default message used when merging pull request // GetDefaultMergeMessage returns default message used when merging pull request
func (pr *PullRequest) GetDefaultMergeMessage() string { func (pr *PullRequest) GetDefaultMergeMessage() string {
if pr.HeadRepo == nil { if pr.HeadRepo == nil {
@ -288,7 +295,7 @@ func (pr *PullRequest) CheckUserAllowedToMerge(doer *User) (err error) {
} }
} }
if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr.BaseBranch, doer); err != nil { if protected, err := pr.BaseRepo.IsProtectedBranchForMerging(pr, pr.BaseBranch, doer); err != nil {
return fmt.Errorf("IsProtectedBranch: %v", err) return fmt.Errorf("IsProtectedBranch: %v", err)
} else if protected { } else if protected {
return ErrNotAllowedToMerge{ return ErrNotAllowedToMerge{

View file

@ -161,6 +161,23 @@ 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

View file

@ -135,13 +135,16 @@ 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 EnableWhitelist bool
WhitelistUsers string WhitelistUsers string
WhitelistTeams string WhitelistTeams string
EnableMergeWhitelist bool EnableMergeWhitelist bool
MergeWhitelistUsers string MergeWhitelistUsers string
MergeWhitelistTeams string MergeWhitelistTeams string
RequiredApprovals int64
ApprovalsWhitelistUsers string
ApprovalsWhitelistTeams string
} }
// Validate validates the fields // Validate validates the fields

View file

@ -859,6 +859,7 @@ pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a>
pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready
pulls.data_broken = This pull request is broken due to missing fork information. pulls.data_broken = This pull request is broken due to missing fork information.
pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments." pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
pulls.blocked_by_approvals = "This Pull Request hasn't enough approvals yet. %d of %d approvals granted."
pulls.can_auto_merge_desc = This pull request can be merged automatically. pulls.can_auto_merge_desc = This pull request can be merged automatically.
pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts. pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts.
pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts. pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
@ -1149,6 +1150,10 @@ settings.protect_merge_whitelist_committers = Enable Merge Whitelist
settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch. settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch.
settings.protect_merge_whitelist_users = Whitelisted users for merging: settings.protect_merge_whitelist_users = Whitelisted users for merging:
settings.protect_merge_whitelist_teams = Whitelisted teams for merging: settings.protect_merge_whitelist_teams = Whitelisted teams for merging:
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_approvals_whitelist_users = Whitelisted reviewers:
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
settings.add_protected_branch = Enable protection settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. settings.update_protect_branch_success = Branch protection for branch '%s' has been updated.
@ -1159,6 +1164,7 @@ settings.default_branch_desc = Select a default repository branch for pull reque
settings.choose_branch = Choose a branch… settings.choose_branch = Choose a branch…
settings.no_protected_branch = There are no protected branches. settings.no_protected_branch = There are no protected branches.
settings.edit_protected_branch = Edit settings.edit_protected_branch = Edit
settings.protected_branch_required_approvals_min = Required approvals cannot be negative.
diff.browse_source = Browse Source diff.browse_source = Browse Source
diff.parent = parent diff.parent = parent

View file

@ -828,6 +828,14 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["MergeStyle"] = "" ctx.Data["MergeStyle"] = ""
} }
} }
if err = pull.LoadProtectedBranch(); err != nil {
ctx.ServerError("LoadProtectedBranch", err)
return
}
if pull.ProtectedBranch != nil {
ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
ctx.Data["GrantedApprovals"] = pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
}
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["PullReviewersWithType"], err = models.GetReviewersByPullID(issue.ID)

View file

@ -124,9 +124,10 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["Users"] = users c.Data["Users"] = users
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",") c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",") c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
if c.Repo.Owner.IsOrganization() { if c.Repo.Owner.IsOrganization() {
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite) teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
if err != nil { if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return return
@ -134,6 +135,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["Teams"] = teams c.Data["Teams"] = teams
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",") c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",") c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",")
} }
c.Data["Branch"] = protectBranch c.Data["Branch"] = protectBranch
@ -164,8 +166,12 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
BranchName: branch, BranchName: branch,
} }
} }
if f.RequiredApprovals < 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
}
var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams []int64 var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
protectBranch.EnableWhitelist = f.EnableWhitelist protectBranch.EnableWhitelist = f.EnableWhitelist
if strings.TrimSpace(f.WhitelistUsers) != "" { if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
@ -180,7 +186,21 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
if strings.TrimSpace(f.MergeWhitelistTeams) != "" { if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
} }
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams) 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, ","))
}
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
})
if err != nil { if err != nil {
ctx.ServerError("UpdateProtectBranch", err) ctx.ServerError("UpdateProtectBranch", err)
return return

View file

@ -39,6 +39,7 @@
{{else if .Issue.IsClosed}}grey {{else if .Issue.IsClosed}}grey
{{else if .IsPullWorkInProgress}}grey {{else if .IsPullWorkInProgress}}grey
{{else if .IsPullRequestBroken}}red {{else if .IsPullRequestBroken}}red
{{else if .IsBlockedByApprovals}}red
{{else if .Issue.PullRequest.IsChecking}}yellow {{else if .Issue.PullRequest.IsChecking}}yellow
{{else if .Issue.PullRequest.CanAutoMerge}}green {{else if .Issue.PullRequest.CanAutoMerge}}green
{{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a> {{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a>
@ -68,6 +69,11 @@
<span class="octicon octicon-x"></span> <span class="octicon octicon-x"></span>
{{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}} {{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}}
</div> </div>
{{else if .IsBlockedByApprovals}}
<div class="item text red">
<span class="octicon octicon-x"></span>
{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
</div>
{{else if .Issue.PullRequest.IsChecking}} {{else if .Issue.PullRequest.IsChecking}}
<div class="item text yellow"> <div class="item text yellow">
<span class="octicon octicon-sync"></span> <span class="octicon octicon-sync"></span>

View file

@ -103,6 +103,47 @@
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="field">
<label for="required-approvals">{{.i18n.Tr "repo.settings.protect_required_approvals"}}</label>
<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}">
<p class="help">{{.i18n.Tr "repo.settings.protect_required_approvals_desc"}}</p>
</div>
<div class="fields">
<div class="whitelist field">
<label>{{.i18n.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="approvals_whitelist_users" value="{{.approvals_whitelist_users}}">
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div>
<div class="menu">
{{range .Users}}
<div class="item" data-value="{{.ID}}">
<img class="ui mini image" src="{{.RelAvatarLink}}">
{{.Name}}
</div>
{{end}}
</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}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
@ -114,4 +155,4 @@
</div> </div>
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}