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