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 & typotokarchuk/v1.17
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"> |
||||
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{template "base/footer" .}} |
Loading…
Reference in new issue