Refuse merge until all required status checks success (#7481)
* refuse merge until ci successfully * deny merge request when required status checkes not succeed on merge Post and API * add database migration for added columns on protected_branch * fix migration * fix protected branch check bug * fix protected branch settings * remove duplicated code on check pull request's required commit statuses pass * remove unused codes * fix migration * add newline for template file * fix go mod * rename function name and some other fixes * fix template * fix bug pull view * remove go1.12 wrong dependencies * add administrator bypass when protected branch status check enabled * fix bug * improve the codes
This commit is contained in:
parent
29454733b4
commit
04ca7f0047
15 changed files with 393 additions and 122 deletions
1
go.mod
1
go.mod
|
@ -80,6 +80,7 @@ require (
|
||||||
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5
|
||||||
github.com/oliamb/cutter v0.2.2
|
github.com/oliamb/cutter v0.2.2
|
||||||
github.com/philhofer/fwd v1.0.0 // indirect
|
github.com/philhofer/fwd v1.0.0 // indirect
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
|
github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e
|
||||||
github.com/prometheus/client_golang v1.1.0
|
github.com/prometheus/client_golang v1.1.0
|
||||||
github.com/prometheus/procfs v0.0.4 // indirect
|
github.com/prometheus/procfs v0.0.4 // indirect
|
||||||
|
|
|
@ -36,6 +36,8 @@ type ProtectedBranch struct {
|
||||||
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"`
|
||||||
|
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||||
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"`
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -205,6 +206,27 @@ func GetLatestCommitStatus(repo *Repository, sha string, page int) ([]*CommitSta
|
||||||
return statuses, x.In("id", ids).Find(&statuses)
|
return statuses, x.In("id", ids).Find(&statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
|
||||||
|
func FindRepoRecentCommitStatusContexts(repoID int64, before time.Duration) ([]string, error) {
|
||||||
|
start := timeutil.TimeStampNow().AddDuration(-before)
|
||||||
|
ids := make([]int64, 0, 10)
|
||||||
|
if err := x.Table("commit_status").
|
||||||
|
Where("repo_id = ?", repoID).
|
||||||
|
And("updated_unix >= ?", start).
|
||||||
|
Select("max( id ) as id").
|
||||||
|
GroupBy("context_hash").OrderBy("max( id ) desc").
|
||||||
|
Find(&ids); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var contexts = make([]string, 0, len(ids))
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return contexts, nil
|
||||||
|
}
|
||||||
|
return contexts, x.Select("context").Table("commit_status").In("id", ids).Find(&contexts)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// NewCommitStatusOptions holds options for creating a CommitStatus
|
// NewCommitStatusOptions holds options for creating a CommitStatus
|
||||||
type NewCommitStatusOptions struct {
|
type NewCommitStatusOptions struct {
|
||||||
Repo *Repository
|
Repo *Repository
|
||||||
|
|
|
@ -242,6 +242,8 @@ var migrations = []Migration{
|
||||||
NewMigration("remove orphaned repository index statuses", removeLingeringIndexStatus),
|
NewMigration("remove orphaned repository index statuses", removeLingeringIndexStatus),
|
||||||
// v93 -> v94
|
// v93 -> v94
|
||||||
NewMigration("add email notification enabled preference to user", addEmailNotificationEnabledToUser),
|
NewMigration("add email notification enabled preference to user", addEmailNotificationEnabledToUser),
|
||||||
|
// v94 -> v95
|
||||||
|
NewMigration("add enable_status_check, status_check_contexts to protected_branch", addStatusCheckColumnsForProtectedBranches),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
24
models/migrations/v94.go
Normal file
24
models/migrations/v94.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// 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 "github.com/go-xorm/xorm"
|
||||||
|
|
||||||
|
func addStatusCheckColumnsForProtectedBranches(x *xorm.Engine) error {
|
||||||
|
type ProtectedBranch struct {
|
||||||
|
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync2(new(ProtectedBranch)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := x.Cols("enable_status_check", "status_check_contexts").Update(&ProtectedBranch{
|
||||||
|
EnableStatusCheck: false,
|
||||||
|
StatusCheckContexts: []string{},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -99,6 +99,20 @@ func (pr *PullRequest) LoadAttributes() error {
|
||||||
return pr.loadAttributes(x)
|
return pr.loadAttributes(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadBaseRepo loads pull request base repository from database
|
||||||
|
func (pr *PullRequest) LoadBaseRepo() error {
|
||||||
|
if pr.BaseRepo == nil {
|
||||||
|
var repo Repository
|
||||||
|
if has, err := x.ID(pr.BaseRepoID).Get(&repo); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return ErrRepoNotExist{ID: pr.BaseRepoID}
|
||||||
|
}
|
||||||
|
pr.BaseRepo = &repo
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadIssue loads issue information from database
|
// LoadIssue loads issue information from database
|
||||||
func (pr *PullRequest) LoadIssue() (err error) {
|
func (pr *PullRequest) LoadIssue() (err error) {
|
||||||
return pr.loadIssue(x)
|
return pr.loadIssue(x)
|
||||||
|
|
|
@ -155,6 +155,8 @@ type ProtectBranchForm struct {
|
||||||
EnableMergeWhitelist bool
|
EnableMergeWhitelist bool
|
||||||
MergeWhitelistUsers string
|
MergeWhitelistUsers string
|
||||||
MergeWhitelistTeams string
|
MergeWhitelistTeams string
|
||||||
|
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
StatusCheckContexts []string
|
||||||
RequiredApprovals int64
|
RequiredApprovals int64
|
||||||
ApprovalsWhitelistUsers string
|
ApprovalsWhitelistUsers string
|
||||||
ApprovalsWhitelistTeams string
|
ApprovalsWhitelistTeams string
|
||||||
|
|
70
modules/pull/commit_status.go
Normal file
70
modules/pull/commit_status.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// 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 pull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsCommitStatusContextSuccess returns true if all required status check contexts succeed.
|
||||||
|
func IsCommitStatusContextSuccess(commitStatuses []*models.CommitStatus, requiredContexts []string) bool {
|
||||||
|
for _, ctx := range requiredContexts {
|
||||||
|
var found bool
|
||||||
|
for _, commitStatus := range commitStatuses {
|
||||||
|
if commitStatus.Context == ctx {
|
||||||
|
if commitStatus.State != models.CommitStatusSuccess {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPullCommitStatusPass returns if all required status checks PASS
|
||||||
|
func IsPullCommitStatusPass(pr *models.PullRequest) (bool, error) {
|
||||||
|
if err := pr.LoadProtectedBranch(); err != nil {
|
||||||
|
return false, errors.Wrap(err, "GetLatestCommitStatus")
|
||||||
|
}
|
||||||
|
if pr.ProtectedBranch == nil || !pr.ProtectedBranch.EnableStatusCheck {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if all required status checks are successful
|
||||||
|
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "OpenRepository")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !headGitRepo.IsBranchExist(pr.HeadBranch) {
|
||||||
|
return false, errors.New("Head branch does not exist, can not merge")
|
||||||
|
}
|
||||||
|
|
||||||
|
sha, err := headGitRepo.GetBranchCommitID(pr.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "GetBranchCommitID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pr.LoadBaseRepo(); err != nil {
|
||||||
|
return false, errors.Wrap(err, "LoadBaseRepo")
|
||||||
|
}
|
||||||
|
|
||||||
|
commitStatuses, err := models.GetLatestCommitStatus(pr.BaseRepo, sha, 0)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "GetLatestCommitStatus")
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsCommitStatusContextSuccess(commitStatuses, pr.ProtectedBranch.StatusCheckContexts), nil
|
||||||
|
}
|
|
@ -981,6 +981,8 @@ pulls.cannot_merge_work_in_progress = This pull request is marked as a work in p
|
||||||
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.files_conflicted = This pull request has changes conflicting with the target branch.
|
pulls.files_conflicted = This pull request has changes conflicting with the target branch.
|
||||||
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.required_status_check_failed = Some required checks were not successful.
|
||||||
|
pulls.required_status_check_administrator = As an administrator, you may still merge this pull request.
|
||||||
pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals yet. %d of %d approvals granted."
|
pulls.blocked_by_approvals = "This Pull Request doesn't have 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.
|
||||||
|
@ -988,6 +990,7 @@ pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
|
||||||
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
|
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
|
||||||
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
|
pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
|
||||||
pulls.no_merge_wip = This pull request can not be merged because it is marked as being a work in progress.
|
pulls.no_merge_wip = This pull request can not be merged because it is marked as being a work in progress.
|
||||||
|
pulls.no_merge_status_check = This pull request cannot be merged because not all required status checkes are successful.
|
||||||
pulls.merge_pull_request = Merge Pull Request
|
pulls.merge_pull_request = Merge Pull Request
|
||||||
pulls.rebase_merge_pull_request = Rebase and Merge
|
pulls.rebase_merge_pull_request = Rebase and Merge
|
||||||
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
|
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
|
||||||
|
@ -1311,6 +1314,9 @@ 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_check_status_contexts = Enable Status Check
|
||||||
|
settings.protect_check_status_contexts_desc = Require status checks to pass before merging Choose which status checks must pass before branches can be merged into a branch that matches this rule. When enabled, commits must first be pushed to another branch, then merged or pushed directly to a branch that matches this rule after status checks have passed.
|
||||||
|
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 of whitelisted users or teams.
|
||||||
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
|
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
"code.gitea.io/gitea/modules/pull"
|
"code.gitea.io/gitea/modules/pull"
|
||||||
|
pull_service "code.gitea.io/gitea/modules/pull"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
milestone_service "code.gitea.io/gitea/services/milestone"
|
milestone_service "code.gitea.io/gitea/services/milestone"
|
||||||
|
@ -571,6 +572,17 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPass, err := pull_service.IsPullCommitStatusPass(pr)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(500, "IsPullCommitStatusPass", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isPass && !ctx.IsUserRepoAdmin() {
|
||||||
|
ctx.Status(405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(form.Do) == 0 {
|
if len(form.Do) == 0 {
|
||||||
form.Do = string(models.MergeStyleMerge)
|
form.Do = string(models.MergeStyleMerge)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
"code.gitea.io/gitea/modules/pull"
|
"code.gitea.io/gitea/modules/pull"
|
||||||
|
pull_service "code.gitea.io/gitea/modules/pull"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
|
@ -322,6 +323,12 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
|
||||||
|
|
||||||
setMergeTarget(ctx, pull)
|
setMergeTarget(ctx, pull)
|
||||||
|
|
||||||
|
if err = pull.LoadProtectedBranch(); err != nil {
|
||||||
|
ctx.ServerError("GetLatestCommitStatus", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
|
||||||
|
|
||||||
var headGitRepo *git.Repository
|
var headGitRepo *git.Repository
|
||||||
var headBranchExist bool
|
var headBranchExist bool
|
||||||
// HeadRepo may be missing
|
// HeadRepo may be missing
|
||||||
|
@ -350,6 +357,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
|
||||||
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
||||||
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
|
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
|
||||||
|
ctx.Data["is_context_required"] = func(context string) bool {
|
||||||
|
for _, c := range pull.ProtectedBranch.StatusCheckContexts {
|
||||||
|
if c == context {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx.Data["IsRequiredStatusCheckSuccess"] = pull_service.IsCommitStatusContextSuccess(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,6 +627,17 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPass, err := pull_service.IsPullCommitStatusPass(pr)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsPullCommitStatusPass", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !isPass && !ctx.IsUserRepoAdmin() {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_status_check"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
|
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
|
||||||
|
|
|
@ -7,6 +7,7 @@ package repo
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
|
@ -125,6 +126,29 @@ func SettingsProtectedBranch(c *context.Context) {
|
||||||
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), ",")
|
c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
|
||||||
|
contexts, _ := models.FindRepoRecentCommitStatusContexts(c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
|
||||||
|
for _, context := range protectBranch.StatusCheckContexts {
|
||||||
|
var found bool
|
||||||
|
for _, ctx := range contexts {
|
||||||
|
if ctx == context {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
contexts = append(contexts, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["branch_status_check_contexts"] = contexts
|
||||||
|
c.Data["is_context_required"] = func(context string) bool {
|
||||||
|
for _, c := range protectBranch.StatusCheckContexts {
|
||||||
|
if c == context {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if c.Repo.Owner.IsOrganization() {
|
if c.Repo.Owner.IsOrganization() {
|
||||||
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
|
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
|
||||||
|
@ -186,6 +210,10 @@ 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, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protectBranch.EnableStatusCheck = f.EnableStatusCheck
|
||||||
|
protectBranch.StatusCheckContexts = f.StatusCheckContexts
|
||||||
|
|
||||||
protectBranch.RequiredApprovals = f.RequiredApprovals
|
protectBranch.RequiredApprovals = f.RequiredApprovals
|
||||||
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
|
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
|
||||||
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
|
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
{{else if .IsFilesConflicted}}grey
|
{{else if .IsFilesConflicted}}grey
|
||||||
{{else if .IsPullRequestBroken}}red
|
{{else if .IsPullRequestBroken}}red
|
||||||
{{else if .IsBlockedByApprovals}}red
|
{{else if .IsBlockedByApprovals}}red
|
||||||
|
{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}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>
|
||||||
|
@ -104,131 +105,151 @@
|
||||||
<span class="octicon octicon-sync"></span>
|
<span class="octicon octicon-sync"></span>
|
||||||
{{$.i18n.Tr "repo.pulls.is_checking"}}
|
{{$.i18n.Tr "repo.pulls.is_checking"}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .Issue.PullRequest.CanAutoMerge}}
|
{{else if and (not .Issue.PullRequest.CanAutoMerge) .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
|
||||||
<div class="item text green">
|
<div class="item text red">
|
||||||
<span class="octicon octicon-check"></span>
|
<span class="octicon octicon-x"></span>
|
||||||
{{$.i18n.Tr "repo.pulls.can_auto_merge_desc"}}
|
{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
|
||||||
</div>
|
</div>
|
||||||
{{if .AllowMerge}}
|
{{else if .Issue.PullRequest.CanAutoMerge}}
|
||||||
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
|
{{if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
|
||||||
{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
|
<div class="item text red">
|
||||||
<div class="ui divider"></div>
|
<span class="octicon octicon-x"></span>
|
||||||
{{if $prUnit.PullRequestsConfig.AllowMerge}}
|
{{$.i18n.Tr "repo.pulls.required_status_check_failed"}}
|
||||||
<div class="ui form merge-fields" style="display: none">
|
</div>
|
||||||
<form action="{{.Link}}/merge" method="post">
|
{{end}}
|
||||||
{{.CsrfTokenHtml}}
|
{{if or $.IsRepoAdmin (not .EnableStatusCheck) .IsRequiredStatusCheckSuccess}}
|
||||||
<div class="field">
|
{{if and $.IsRepoAdmin .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
|
||||||
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
|
<div class="item text yellow">
|
||||||
</div>
|
<span class="octicon octicon-primitive-dot"></span>
|
||||||
<div class="field">
|
{{$.i18n.Tr "repo.pulls.required_status_check_administrator"}}
|
||||||
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="ui green button" type="submit" name="do" value="merge">
|
|
||||||
{{$.i18n.Tr "repo.pulls.merge_pull_request"}}
|
|
||||||
</button>
|
|
||||||
<button class="ui button merge-cancel">
|
|
||||||
{{$.i18n.Tr "cancel"}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowRebase}}
|
|
||||||
<div class="ui form rebase-fields" style="display: none">
|
|
||||||
<form action="{{.Link}}/merge" method="post">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<button class="ui green button" type="submit" name="do" value="rebase">
|
|
||||||
{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}
|
|
||||||
</button>
|
|
||||||
<button class="ui button merge-cancel">
|
|
||||||
{{$.i18n.Tr "cancel"}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowRebaseMerge}}
|
|
||||||
<div class="ui form rebase-merge-fields" style="display: none">
|
|
||||||
<form action="{{.Link}}/merge" method="post">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<div class="field">
|
|
||||||
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="ui green button" type="submit" name="do" value="rebase-merge">
|
|
||||||
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
|
||||||
</button>
|
|
||||||
<button class="ui button merge-cancel">
|
|
||||||
{{$.i18n.Tr "cancel"}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowSquash}}
|
|
||||||
<div class="ui form squash-fields" style="display: none">
|
|
||||||
<form action="{{.Link}}/merge" method="post">
|
|
||||||
{{.CsrfTokenHtml}}
|
|
||||||
<div class="field">
|
|
||||||
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultSquashMessage}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="ui green button" type="submit" name="do" value="squash">
|
|
||||||
{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}
|
|
||||||
</button>
|
|
||||||
<button class="ui button merge-cancel">
|
|
||||||
{{$.i18n.Tr "cancel"}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="ui green buttons merge-button">
|
|
||||||
<button class="ui button" data-do="{{.MergeStyle}}">
|
|
||||||
<span class="octicon octicon-git-merge"></span>
|
|
||||||
<span class="button-text">
|
|
||||||
{{if eq .MergeStyle "merge"}}
|
|
||||||
{{$.i18n.Tr "repo.pulls.merge_pull_request"}}
|
|
||||||
{{end}}
|
|
||||||
{{if eq .MergeStyle "rebase"}}
|
|
||||||
{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}
|
|
||||||
{{end}}
|
|
||||||
{{if eq .MergeStyle "rebase-merge"}}
|
|
||||||
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
|
||||||
{{end}}
|
|
||||||
{{if eq .MergeStyle "squash"}}
|
|
||||||
{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<div class="ui dropdown icon button">
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
<div class="menu">
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowMerge}}
|
|
||||||
<div class="item{{if eq .MergeStyle "merge"}} active selected{{end}}" data-do="merge">{{$.i18n.Tr "repo.pulls.merge_pull_request"}}</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowRebase}}
|
|
||||||
<div class="item{{if eq .MergeStyle "rebase"}} active selected{{end}}" data-do="rebase">{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowRebaseMerge}}
|
|
||||||
<div class="item{{if eq .MergeStyle "rebase-merge"}} active selected{{end}}" data-do="rebase-merge">{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
|
|
||||||
{{end}}
|
|
||||||
{{if $prUnit.PullRequestsConfig.AllowSquash}}
|
|
||||||
<div class="item{{if eq .MergeStyle "squash"}} active selected{{end}}" data-do="squash">{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="item text red">
|
<div class="item text green">
|
||||||
<span class="octicon octicon-x"></span>
|
<span class="octicon octicon-check"></span>
|
||||||
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
|
{{$.i18n.Tr "repo.pulls.can_auto_merge_desc"}}
|
||||||
</div>
|
|
||||||
<div class="item text grey">
|
|
||||||
<span class="octicon octicon-info"></span>
|
|
||||||
{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .AllowMerge}}
|
||||||
|
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
|
||||||
|
{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowMerge}}
|
||||||
|
<div class="ui form merge-fields" style="display: none">
|
||||||
|
<form action="{{.Link}}/merge" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="ui green button" type="submit" name="do" value="merge">
|
||||||
|
{{$.i18n.Tr "repo.pulls.merge_pull_request"}}
|
||||||
|
</button>
|
||||||
|
<button class="ui button merge-cancel">
|
||||||
|
{{$.i18n.Tr "cancel"}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowRebase}}
|
||||||
|
<div class="ui form rebase-fields" style="display: none">
|
||||||
|
<form action="{{.Link}}/merge" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button class="ui green button" type="submit" name="do" value="rebase">
|
||||||
|
{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}
|
||||||
|
</button>
|
||||||
|
<button class="ui button merge-cancel">
|
||||||
|
{{$.i18n.Tr "cancel"}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowRebaseMerge}}
|
||||||
|
<div class="ui form rebase-merge-fields" style="display: none">
|
||||||
|
<form action="{{.Link}}/merge" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="ui green button" type="submit" name="do" value="rebase-merge">
|
||||||
|
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
||||||
|
</button>
|
||||||
|
<button class="ui button merge-cancel">
|
||||||
|
{{$.i18n.Tr "cancel"}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowSquash}}
|
||||||
|
<div class="ui form squash-fields" style="display: none">
|
||||||
|
<form action="{{.Link}}/merge" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultSquashMessage}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="ui green button" type="submit" name="do" value="squash">
|
||||||
|
{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}
|
||||||
|
</button>
|
||||||
|
<button class="ui button merge-cancel">
|
||||||
|
{{$.i18n.Tr "cancel"}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="ui green buttons merge-button">
|
||||||
|
<button class="ui button" data-do="{{.MergeStyle}}">
|
||||||
|
<span class="octicon octicon-git-merge"></span>
|
||||||
|
<span class="button-text">
|
||||||
|
{{if eq .MergeStyle "merge"}}
|
||||||
|
{{$.i18n.Tr "repo.pulls.merge_pull_request"}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .MergeStyle "rebase"}}
|
||||||
|
{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .MergeStyle "rebase-merge"}}
|
||||||
|
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .MergeStyle "squash"}}
|
||||||
|
{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="ui dropdown icon button">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="menu">
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowMerge}}
|
||||||
|
<div class="item{{if eq .MergeStyle "merge"}} active selected{{end}}" data-do="merge">{{$.i18n.Tr "repo.pulls.merge_pull_request"}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowRebase}}
|
||||||
|
<div class="item{{if eq .MergeStyle "rebase"}} active selected{{end}}" data-do="rebase">{{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowRebaseMerge}}
|
||||||
|
<div class="item{{if eq .MergeStyle "rebase-merge"}} active selected{{end}}" data-do="rebase-merge">{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if $prUnit.PullRequestsConfig.AllowSquash}}
|
||||||
|
<div class="item{{if eq .MergeStyle "squash"}} active selected{{end}}" data-do="squash">{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="item text red">
|
||||||
|
<span class="octicon octicon-x"></span>
|
||||||
|
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
|
||||||
|
</div>
|
||||||
|
<div class="item text grey">
|
||||||
|
<span class="octicon octicon-info"></span>
|
||||||
|
{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="item text red">
|
<div class="item text red">
|
||||||
|
|
|
@ -15,7 +15,12 @@
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<span>{{template "repo/commit_status" .}}</span>
|
<span>{{template "repo/commit_status" .}}</span>
|
||||||
<span class="ui">{{.Context}} <span class="text grey">{{.Description}}</span></span>
|
<span class="ui">{{.Context}} <span class="text grey">{{.Description}}</span></span>
|
||||||
<div class="ui right">{{if .TargetURL}}<a href="{{.TargetURL}}">Details</a>{{end}}</div>
|
<div class="ui right">
|
||||||
|
{{if $.is_context_required}}
|
||||||
|
{{if (call $.is_context_required .Context)}}<div class="ui label">Required</div>{{end}}
|
||||||
|
{{end}}
|
||||||
|
<span class="ui">{{if .TargetURL}}<a href="{{.TargetURL}}">Details</a>{{end}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -104,6 +104,38 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input class="enable-statuscheck" name="enable_status_check" type="checkbox" data-target="#statuscheck_contexts_box" {{if .Branch.EnableStatusCheck}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.settings.protect_check_status_contexts"}}</label>
|
||||||
|
<p class="help">{{.i18n.Tr "repo.settings.protect_check_status_contexts_desc"}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statuscheck_contexts_box" class="fields {{if not .Branch.EnableStatusCheck}}disabled{{end}}">
|
||||||
|
<div class="field">
|
||||||
|
<table class="ui celled table six column">
|
||||||
|
<thead>
|
||||||
|
<tr><th>
|
||||||
|
{{.i18n.Tr "repo.settings.protect_check_status_contexts_list"}}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $.branch_status_check_contexts}}
|
||||||
|
<tr><td>
|
||||||
|
<span class="ui checkbox">
|
||||||
|
<input class="enable-whitelist" name="status_check_contexts" value="{{.}}" type="checkbox" {{if $.is_context_require}}{{if call $.is_context_required .}}checked{{end}}{{end}}>
|
||||||
|
</span>
|
||||||
|
{{.}}
|
||||||
|
{{if $.is_context_required}}{{if call $.is_context_required .}}<div class="ui label right">Required</div>{{end}}{{end}}
|
||||||
|
</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="required-approvals">{{.i18n.Tr "repo.settings.protect_required_approvals"}}</label>
|
<label for="required-approvals">{{.i18n.Tr "repo.settings.protect_required_approvals"}}</label>
|
||||||
<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}">
|
<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}">
|
||||||
|
|
Reference in a new issue