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:
parent
64680b72bd
commit
9681c83734
13 changed files with 251 additions and 41 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
16
models/migrations/v74.go
Normal 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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
// ___________ __________
|
// ___________ __________
|
||||||
// \__ ___/___ _____ _____\______ \ ____ ______ ____
|
// \__ ___/___ _____ _____\______ \ ____ ______ ____
|
||||||
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \
|
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" .}}
|
||||||
|
|
Reference in a new issue