improve protected branch to add whitelist support (#2451)
* improve protected branch to add whitelist support * fix lint * fix style check * fix tests * fix description on UI and import * fix test * bug fixed * fix tests and languages * move isSliceInt64Eq to util pkg; improve function names & typorelease/v1.3
parent
be3319b3d5
commit
1739e84ac0
@ -0,0 +1,55 @@
|
||||
// Copyright 2017 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 (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func migrateProtectedBranchStruct(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
BranchName string `xorm:"UNIQUE(s)"`
|
||||
CanPush bool
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
var pbs []ProtectedBranch
|
||||
err := x.Find(&pbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pb := range pbs {
|
||||
if pb.CanPush {
|
||||
if _, err = x.ID(pb.ID).Delete(new(ProtectedBranch)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case setting.UseSQLite3:
|
||||
log.Warn("Unable to drop columns in SQLite")
|
||||
case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB:
|
||||
if _, err := x.Exec("ALTER TABLE protected_branch DROP COLUMN can_push"); err != nil {
|
||||
return fmt.Errorf("DROP COLUMN can_push: %v", err)
|
||||
}
|
||||
default:
|
||||
log.Fatal(4, "Unrecognized DB")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// Copyright 2017 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 util
|
||||
|
||||
import "sort"
|
||||
|
||||
// Int64Slice attaches the methods of Interface to []int64, sorting in increasing order.
|
||||
type Int64Slice []int64
|
||||
|
||||
func (p Int64Slice) Len() int { return len(p) }
|
||||
func (p Int64Slice) Less(i, j int) bool { return p[i] < p[j] }
|
||||
func (p Int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// IsSliceInt64Eq returns if the two slice has the same elements but different sequences.
|
||||
func IsSliceInt64Eq(a, b []int64) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
sort.Sort(Int64Slice(a))
|
||||
sort.Sort(Int64Slice(b))
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
// Copyright 2017 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 repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// ProtectedBranch render the page to protect the repository
|
||||
func ProtectedBranch(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["PageIsSettingsBranches"] = true
|
||||
|
||||
protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
|
||||
if err != nil {
|
||||
ctx.Handle(500, "GetProtectedBranches", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["ProtectedBranches"] = protectedBranches
|
||||
|
||||
branches := ctx.Data["Branches"].([]string)
|
||||
leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
|
||||
for _, b := range branches {
|
||||
var protected bool
|
||||
for _, pb := range protectedBranches {
|
||||
if b == pb.BranchName {
|
||||
protected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !protected {
|
||||
leftBranches = append(leftBranches, b)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["LeftBranches"] = leftBranches
|
||||
|
||||
ctx.HTML(200, tplBranches)
|
||||
}
|
||||
|
||||
// ProtectedBranchPost response for protect for a branch of a repository
|
||||
func ProtectedBranchPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["PageIsSettingsBranches"] = true
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
switch ctx.Query("action") {
|
||||
case "default_branch":
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(200, tplBranches)
|
||||
return
|
||||
}
|
||||
|
||||
branch := ctx.Query("branch")
|
||||
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
|
||||
ctx.Status(404)
|
||||
return
|
||||
} else if repo.DefaultBranch != branch {
|
||||
repo.DefaultBranch = branch
|
||||
if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
|
||||
if !git.IsErrUnsupportedVersion(err) {
|
||||
ctx.Handle(500, "SetDefaultBranch", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := repo.UpdateDefaultBranch(); err != nil {
|
||||
ctx.Handle(500, "SetDefaultBranch", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
|
||||
default:
|
||||
ctx.Handle(404, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SettingsProtectedBranch renders the protected branch setting page
|
||||
func SettingsProtectedBranch(c *context.Context) {
|
||||
branch := c.Params("*")
|
||||
if !c.Repo.GitRepo.IsBranchExist(branch) {
|
||||
c.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["Title"] = c.Tr("repo.settings.protected_branches") + " - " + branch
|
||||
c.Data["PageIsSettingsBranches"] = true
|
||||
|
||||
protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
|
||||
if err != nil {
|
||||
if !models.IsErrBranchNotExist(err) {
|
||||
c.Handle(500, "GetProtectBranchOfRepoByName", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if protectBranch == nil {
|
||||
// No options found, create defaults.
|
||||
protectBranch = &models.ProtectedBranch{
|
||||
BranchName: branch,
|
||||
}
|
||||
}
|
||||
|
||||
users, err := c.Repo.Repository.GetWriters()
|
||||
if err != nil {
|
||||
c.Handle(500, "Repo.Repository.GetWriters", err)
|
||||
return
|
||||
}
|
||||
c.Data["Users"] = users
|
||||
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
|
||||
|
||||
if c.Repo.Owner.IsOrganization() {
|
||||
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite)
|
||||
if err != nil {
|
||||
c.Handle(500, "Repo.Owner.TeamsWithAccessToRepo", err)
|
||||
return
|
||||
}
|
||||
c.Data["Teams"] = teams
|
||||
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
|
||||
}
|
||||
|
||||
c.Data["Branch"] = protectBranch
|
||||
c.HTML(200, tplProtectedBranch)
|
||||
}
|
||||
|
||||
// SettingsProtectedBranchPost updates the protected branch settings
|
||||
func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm) {
|
||||
branch := ctx.Params("*")
|
||||
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
|
||||
if err != nil {
|
||||
if !models.IsErrBranchNotExist(err) {
|
||||
ctx.Handle(500, "GetProtectBranchOfRepoByName", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.Protected {
|
||||
if protectBranch == nil {
|
||||
// No options found, create defaults.
|
||||
protectBranch = &models.ProtectedBranch{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
BranchName: branch,
|
||||
}
|
||||
}
|
||||
|
||||
protectBranch.EnableWhitelist = f.EnableWhitelist
|
||||
whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
|
||||
whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
|
||||
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "UpdateProtectBranch", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
|
||||
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
|
||||
} else {
|
||||
if protectBranch != nil {
|
||||
if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil {
|
||||
ctx.Handle(500, "DeleteProtectedBranch", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
|
||||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="repository settings branches">
|
||||
{{template "repo/header" .}}
|
||||
{{template "repo/settings/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<h4 class="ui top attached header">
|
||||
{{.i18n.Tr "repo.settings.branch_protection" .Branch.BranchName | Str2html}}
|
||||
</h4>
|
||||
<div class="ui attached segment branch-protection">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input class="enable-protection" name="protected" type="checkbox" data-target="#protection_box" {{if .Branch.IsProtected}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.protect_this_branch"}}</label>
|
||||
<p class="help">{{.i18n.Tr "repo.settings.protect_this_branch_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="protection_box" class="fields {{if not .Branch.IsProtected}}disabled{{end}}">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input class="enable-whitelist" name="enable_whitelist" type="checkbox" data-target="#whitelist_box" {{if .Branch.EnableWhitelist}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label>
|
||||
<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="whitelist_box" class="fields {{if not .Branch.EnableWhitelist}}disabled{{end}}">
|
||||
<div class="whitelist field">
|
||||
<label>{{.i18n.Tr "repo.settings.protect_whitelist_users"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="whitelist_users" value="{{.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_whitelist_teams"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="whitelist_teams" value="{{.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 class="ui divider"></div>
|
||||
|
||||
<div class="field">
|
||||