summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2017-09-14 16:16:22 +0800
committerGitHub <noreply@github.com>2017-09-14 16:16:22 +0800
commit1739e84ac02c0384c04576a00abab9348293f9c7 (patch)
tree1015e68f36421f274d2e883ff3ddb0cb29b6af71
parentbe3319b3d545289b772d7a92b4b62205863954d9 (diff)
downloadgitea-1739e84ac02c0384c04576a00abab9348293f9c7.tar.gz
gitea-1739e84ac02c0384c04576a00abab9348293f9c7.zip
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 & typo
-rw-r--r--cmd/hook.go48
-rw-r--r--integrations/editor_test.go25
-rw-r--r--integrations/integration_test.go2
-rw-r--r--integrations/internal_test.go2
-rw-r--r--models/branches.go192
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v40.go55
-rw-r--r--models/org.go5
-rw-r--r--models/org_team.go20
-rw-r--r--models/repo.go36
-rw-r--r--modules/auth/repo_form.go20
-rw-r--r--modules/base/tool.go10
-rw-r--r--modules/context/repo.go4
-rw-r--r--modules/private/branch.go26
-rw-r--r--modules/util/compare.go29
-rw-r--r--options/locale/locale_en-US.ini14
-rw-r--r--public/css/index.css24
-rw-r--r--public/js/index.js45
-rw-r--r--public/less/_repository.less33
-rw-r--r--routers/private/branch.go24
-rw-r--r--routers/private/internal.go1
-rw-r--r--routers/repo/editor.go2
-rw-r--r--routers/repo/issue.go2
-rw-r--r--routers/repo/pull.go2
-rw-r--r--routers/repo/setting.go138
-rw-r--r--routers/repo/setting_protected_branch.go186
-rw-r--r--routers/routes/routes.go4
-rw-r--r--templates/repo/settings/branches.tmpl14
-rw-r--r--templates/repo/settings/protected_branch.tmpl74
29 files changed, 736 insertions, 303 deletions
diff --git a/cmd/hook.go b/cmd/hook.go
index 06250181d3..0ddbb36f90 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -84,9 +84,10 @@ func runHookPreReceive(c *cli.Context) error {
// the environment setted on serv command
repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
- //username := os.Getenv(models.EnvRepoUsername)
- //reponame := os.Getenv(models.EnvRepoName)
- //repoPath := models.RepoPath(username, reponame)
+ username := os.Getenv(models.EnvRepoUsername)
+ reponame := os.Getenv(models.EnvRepoName)
+ userIDStr := os.Getenv(models.EnvPusherID)
+ repoPath := models.RepoPath(username, reponame)
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(os.Stdin)
@@ -104,36 +105,37 @@ func runHookPreReceive(c *cli.Context) error {
continue
}
- //oldCommitID := string(fields[0])
+ oldCommitID := string(fields[0])
newCommitID := string(fields[1])
refFullName := string(fields[2])
- // FIXME: when we add feature to protected branch to deny force push, then uncomment below
- /*var isForce bool
- // detect force push
- if git.EmptySHA != oldCommitID {
- output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
- if err != nil {
- fail("Internal error", "Fail to detect force push: %v", err)
- } else if len(output) > 0 {
- isForce = true
- }
- }*/
-
branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
protectBranch, err := private.GetProtectedBranchBy(repoID, branchName)
if err != nil {
log.GitLogger.Fatal(2, "retrieve protected branches information failed")
}
- if protectBranch != nil {
- if !protectBranch.CanPush {
- // check and deletion
- if newCommitID == git.EmptySHA {
- fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
- } else {
+ if protectBranch != nil && protectBranch.IsProtected() {
+ // detect force push
+ if git.EmptySHA != oldCommitID {
+ output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
+ if err != nil {
+ fail("Internal error", "Fail to detect force push: %v", err)
+ } else if len(output) > 0 {
+ fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
+ }
+ }
+
+ // check and deletion
+ if newCommitID == git.EmptySHA {
+ fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
+ } else {
+ userID, _ := strconv.ParseInt(userIDStr, 10, 64)
+ canPush, err := private.CanUserPush(protectBranch.ID, userID)
+ if err != nil {
+ fail("Internal error", "Fail to detect user can push: %v", err)
+ } else if !canPush {
fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "")
- //fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
}
}
}
diff --git a/integrations/editor_test.go b/integrations/editor_test.go
index 79b6bb790a..cc94edfd3f 100644
--- a/integrations/editor_test.go
+++ b/integrations/editor_test.go
@@ -43,16 +43,15 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
// Change master branch to protected
- req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches?action=protected_branch", map[string]string{
- "_csrf": csrf,
- "branchName": "master",
- "canPush": "true",
+ req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
+ "_csrf": csrf,
+ "protected": "on",
})
- resp := session.MakeRequest(t, req, http.StatusOK)
+ resp := session.MakeRequest(t, req, http.StatusFound)
// Check if master branch has been locked successfully
flashCookie := session.GetCookie("macaron_flash")
assert.NotNil(t, flashCookie)
- assert.EqualValues(t, flashCookie.Value, "success%3Dmaster%2BLocked%2Bsuccessfully")
+ assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bchanged%2Bsuccessfully.", flashCookie.Value)
// Request editor page
req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
@@ -74,6 +73,20 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK)
// Check body for error message
assert.Contains(t, string(resp.Body), "Can not commit to protected branch &#39;master&#39;.")
+
+ // remove the protected branch
+ csrf = GetCSRF(t, session, "/user2/repo1/settings/branches")
+ // Change master branch to protected
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
+ "_csrf": csrf,
+ "protected": "off",
+ })
+ resp = session.MakeRequest(t, req, http.StatusFound)
+ // Check if master branch has been locked successfully
+ flashCookie = session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bremoved%2Bsuccessfully", flashCookie.Value)
+
}
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath string) *TestResponse {
diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index d43f3977b8..0b5d8a764d 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -269,7 +269,7 @@ func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestRespo
mac.ServeHTTP(respWriter, req)
if expectedStatus != NoExpectedStatus {
assert.EqualValues(t, expectedStatus, respWriter.HeaderCode,
- "Request URL: %s", req.URL.String())
+ "Request URL: %s %s", req.URL.String(), buffer.String())
}
return &TestResponse{
HeaderCode: respWriter.HeaderCode,
diff --git a/integrations/internal_test.go b/integrations/internal_test.go
index c22e951bc5..d58b8b0b4e 100644
--- a/integrations/internal_test.go
+++ b/integrations/internal_test.go
@@ -31,7 +31,7 @@ func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr,
var branch models.ProtectedBranch
t.Log(string(resp.Body))
assert.NoError(t, json.Unmarshal(resp.Body, &branch))
- assert.Equal(t, canPush, branch.CanPush)
+ assert.Equal(t, canPush, !branch.IsProtected())
}
}
diff --git a/models/branches.go b/models/branches.go
index 4461da0067..1c3c0d17be 100644
--- a/models/branches.go
+++ b/models/branches.go
@@ -8,6 +8,12 @@ import (
"fmt"
"strings"
"time"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/Unknwon/com"
)
const (
@@ -17,14 +23,43 @@ const (
// ProtectedBranch struct
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 `xorm:"created"`
- Updated time.Time `xorm:"-"`
- UpdatedUnix int64 `xorm:"updated"`
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"UNIQUE(s)"`
+ BranchName string `xorm:"UNIQUE(s)"`
+ EnableWhitelist bool
+ WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
+ WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
+ Created time.Time `xorm:"-"`
+ CreatedUnix int64 `xorm:"created"`
+ Updated time.Time `xorm:"-"`
+ UpdatedUnix int64 `xorm:"updated"`
+}
+
+// IsProtected returns if the branch is protected
+func (protectBranch *ProtectedBranch) IsProtected() bool {
+ return protectBranch.ID > 0
+}
+
+// CanUserPush returns if some user could push to this protected branch
+func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
+ if !protectBranch.EnableWhitelist {
+ return false
+ }
+
+ if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
+ return true
+ }
+
+ if len(protectBranch.WhitelistTeamIDs) == 0 {
+ return false
+ }
+
+ in, err := IsUserInTeams(userID, protectBranch.WhitelistTeamIDs)
+ if err != nil {
+ log.Error(1, "IsUserInTeams:", err)
+ return false
+ }
+ return in
}
// GetProtectedBranchByRepoID getting protected branch by repo ID
@@ -46,6 +81,73 @@ func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, er
return rel, nil
}
+// GetProtectedBranchByID getting protected branch by ID
+func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
+ rel := &ProtectedBranch{ID: id}
+ has, err := x.Get(rel)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, nil
+ }
+ return rel, nil
+}
+
+// UpdateProtectBranch saves branch protection options of repository.
+// 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
+// to avoid unnecessary whitelist delete and regenerate.
+func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs []int64) (err error) {
+ if err = repo.GetOwner(); err != nil {
+ return fmt.Errorf("GetOwner: %v", err)
+ }
+
+ hasUsersChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistUserIDs, whitelistUserIDs)
+ if hasUsersChanged {
+ protectBranch.WhitelistUserIDs = make([]int64, 0, len(whitelistUserIDs))
+ for _, userID := range whitelistUserIDs {
+ has, err := hasAccess(x, userID, repo, AccessModeWrite)
+ if err != nil {
+ return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
+ } else if !has {
+ continue // Drop invalid user ID
+ }
+
+ protectBranch.WhitelistUserIDs = append(protectBranch.WhitelistUserIDs, userID)
+ }
+ }
+
+ // if the repo is in an orgniziation
+ hasTeamsChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
+ if hasTeamsChanged {
+ teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
+ if err != nil {
+ return fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
+ }
+ protectBranch.WhitelistTeamIDs = make([]int64, 0, len(teams))
+ for i := range teams {
+ if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(whitelistTeamIDs, teams[i].ID) {
+ protectBranch.WhitelistTeamIDs = append(protectBranch.WhitelistTeamIDs, teams[i].ID)
+ }
+ }
+ }
+
+ // Make sure protectBranch.ID is not 0 for whitelists
+ if protectBranch.ID == 0 {
+ if _, err = x.Insert(protectBranch); err != nil {
+ return fmt.Errorf("Insert: %v", err)
+ }
+ return nil
+ }
+
+ if _, err = x.Id(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
+ return fmt.Errorf("Update: %v", err)
+ }
+
+ return nil
+}
+
// GetProtectedBranches get all protected branches
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0)
@@ -53,7 +155,7 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
}
// IsProtectedBranch checks if branch is protected
-func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) {
+func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) {
protectedBranch := &ProtectedBranch{
RepoID: repo.ID,
BranchName: branchName,
@@ -63,70 +165,12 @@ func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) {
if err != nil {
return true, err
} else if has {
- return true, nil
+ return !protectedBranch.CanUserPush(doer.ID), nil
}
return false, nil
}
-// AddProtectedBranch add protection to branch
-func (repo *Repository) AddProtectedBranch(branchName string, canPush bool) error {
- protectedBranch := &ProtectedBranch{
- RepoID: repo.ID,
- BranchName: branchName,
- }
-
- has, err := x.Get(protectedBranch)
- if err != nil {
- return err
- } else if has {
- return nil
- }
-
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
- return err
- }
- protectedBranch.CanPush = canPush
- if _, err = sess.InsertOne(protectedBranch); err != nil {
- return err
- }
-
- return sess.Commit()
-}
-
-// ChangeProtectedBranch access mode sets new access mode for the ProtectedBranch.
-func (repo *Repository) ChangeProtectedBranch(id int64, canPush bool) error {
- ProtectedBranch := &ProtectedBranch{
- RepoID: repo.ID,
- ID: id,
- }
- has, err := x.Get(ProtectedBranch)
- if err != nil {
- return fmt.Errorf("get ProtectedBranch: %v", err)
- } else if !has {
- return nil
- }
-
- if ProtectedBranch.CanPush == canPush {
- return nil
- }
- ProtectedBranch.CanPush = canPush
-
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
- return err
- }
-
- if _, err = sess.Id(ProtectedBranch.ID).AllCols().Update(ProtectedBranch); err != nil {
- return fmt.Errorf("update ProtectedBranch: %v", err)
- }
-
- return sess.Commit()
-}
-
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
protectedBranch := &ProtectedBranch{
@@ -148,15 +192,3 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
return sess.Commit()
}
-
-// newProtectedBranch insert one queue
-func newProtectedBranch(protectedBranch *ProtectedBranch) error {
- _, err := x.InsertOne(protectedBranch)
- return err
-}
-
-// UpdateProtectedBranch update queue
-func UpdateProtectedBranch(protectedBranch *ProtectedBranch) error {
- _, err := x.Update(protectedBranch)
- return err
-}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index a796c6d6af..e7542954d7 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -128,6 +128,8 @@ var migrations = []Migration{
NewMigration("remove commits and settings unit types", removeCommitsUnitType),
// v39 -> v40
NewMigration("adds time tracking and stopwatches", addTimetracking),
+ // v40 -> v41
+ NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
}
// Migrate database to current version
diff --git a/models/migrations/v40.go b/models/migrations/v40.go
new file mode 100644
index 0000000000..324521e0b6
--- /dev/null
+++ b/models/migrations/v40.go
@@ -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
+}
diff --git a/models/org.go b/models/org.go
index d43f15f9aa..fd81753931 100644
--- a/models/org.go
+++ b/models/org.go
@@ -577,6 +577,11 @@ func (org *User) getUserTeamIDs(e Engine, userID int64) ([]int64, error) {
Find(&teamIDs)
}
+// TeamsWithAccessToRepo returns all teamsthat have given access level to the repository.
+func (org *User) TeamsWithAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
+ return GetTeamsWithAccessToRepo(org.ID, repoID, mode)
+}
+
// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) {
return org.getUserTeamIDs(x, userID)
diff --git a/models/org_team.go b/models/org_team.go
index bc0e12b504..acddc70b58 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -35,6 +35,11 @@ func (t *Team) GetUnitTypes() []UnitType {
return t.UnitTypes
}
+// HasWriteAccess returns true if team has at least write level access mode.
+func (t *Team) HasWriteAccess() bool {
+ return t.Authorize >= AccessModeWrite
+}
+
// IsOwnerTeam returns true if team is owner team.
func (t *Team) IsOwnerTeam() bool {
return t.Name == ownerTeamName
@@ -594,6 +599,11 @@ func RemoveTeamMember(team *Team, userID int64) error {
return sess.Commit()
}
+// IsUserInTeams returns if a user in some teams
+func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
+ return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
+}
+
// ___________ __________
// \__ ___/___ _____ _____\______ \ ____ ______ ____
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \
@@ -639,3 +649,13 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error {
})
return err
}
+
+// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
+func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
+ teams := make([]*Team, 0, 5)
+ return teams, x.Where("team.authorize >= ?", mode).
+ Join("INNER", "team_repo", "team_repo.team_id = team.id").
+ And("team_repo.org_id = ?", orgID).
+ And("team_repo.repo_id = ?", repoID).
+ Find(&teams)
+}
diff --git a/models/repo.go b/models/repo.go
index a2e63e2af7..8d5b3b87c7 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -656,6 +656,42 @@ func (repo *Repository) CanEnableEditor() bool {
return !repo.IsMirror
}
+// GetWriters returns all users that have write access to the repository.
+func (repo *Repository) GetWriters() (_ []*User, err error) {
+ return repo.getUsersWithAccessMode(x, AccessModeWrite)
+}
+
+// getUsersWithAccessMode returns users that have at least given access mode to the repository.
+func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []*User, err error) {
+ if err = repo.getOwner(e); err != nil {
+ return nil, err
+ }
+
+ accesses := make([]*Access, 0, 10)
+ if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
+ return nil, err
+ }
+
+ // Leave a seat for owner itself to append later, but if owner is an organization
+ // and just waste 1 unit is cheaper than re-allocate memory once.
+ users := make([]*User, 0, len(accesses)+1)
+ if len(accesses) > 0 {
+ userIDs := make([]int64, len(accesses))
+ for i := 0; i < len(accesses); i++ {
+ userIDs[i] = accesses[i].UserID
+ }
+
+ if err = e.In("id", userIDs).Find(&users); err != nil {
+ return nil, err
+ }
+ }
+ if !repo.Owner.IsOrganization() {
+ users = append(users, repo.Owner)
+ }
+
+ return users, nil
+}
+
// NextIssueIndex returns the next issue index
// FIXME: should have a mutex to prevent producing same index for two issues that are created
// closely enough.
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 4e9d2bff61..a6454655f7 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -113,6 +113,26 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
return validate(errs, ctx.Data, f, ctx.Locale)
}
+// __________ .__
+// \______ \____________ ____ ____ | |__
+// | | _/\_ __ \__ \ / \_/ ___\| | \
+// | | \ | | \// __ \| | \ \___| Y \
+// |______ / |__| (____ /___| /\___ >___| /
+// \/ \/ \/ \/ \/
+
+// ProtectBranchForm form for changing protected branch settings
+type ProtectBranchForm struct {
+ Protected bool
+ EnableWhitelist bool
+ WhitelistUsers string
+ WhitelistTeams string
+}
+
+// Validate validates the fields
+func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+ return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
// __ __ ___. .__ .__ __
// / \ / \ ____\_ |__ | |__ | |__ ____ | | __
// \ \/\/ // __ \| __ \| | \| | \ / _ \| |/ /
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 543775e0df..26ced075da 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -497,6 +497,16 @@ func Int64sToMap(ints []int64) map[int64]bool {
return m
}
+// Int64sContains returns if a int64 in a slice of int64
+func Int64sContains(intsSlice []int64, a int64) bool {
+ for _, c := range intsSlice {
+ if c == a {
+ return true
+ }
+ }
+ return false
+}
+
// IsLetter reports whether the rune is a letter (category L).
// https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257
func IsLetter(ch rune) bool {
diff --git a/modules/context/repo.go b/modules/context/repo.go
index e335eafde3..a82535a6d4 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -78,8 +78,8 @@ func (r *Repository) CanEnableEditor() bool {
// CanCommitToBranch returns true if repository is editable and user has proper access level
// and branch is not protected
-func (r *Repository) CanCommitToBranch() (bool, error) {
- protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName)
+func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) {
+ protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName, doer)
if err != nil {
return false, err
}
diff --git a/modules/private/branch.go b/modules/private/branch.go
index 6b3b9170bb..fed66d29ff 100644
--- a/modules/private/branch.go
+++ b/modules/private/branch.go
@@ -38,3 +38,29 @@ func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBra
return &branch, nil
}
+
+// CanUserPush returns if user can push
+func CanUserPush(protectedBranchID, userID int64) (bool, error) {
+ // Ask for running deliver hook and test pull request tasks.
+ reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranch/%d/%d", protectedBranchID, userID)
+ log.GitLogger.Trace("CanUserPush: %s", reqURL)
+
+ resp, err := newInternalRequest(reqURL, "GET").Response()
+ if err != nil {
+ return false, err
+ }
+
+ var canPush = make(map[string]interface{})
+ if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil {
+ return false, err
+ }
+
+ defer resp.Body.Close()
+
+ // All 2XX status codes are accepted and others will return an error
+ if resp.StatusCode/100 != 2 {
+ return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err)
+ }
+
+ return canPush["can_push"].(bool), nil
+}
diff --git a/modules/util/compare.go b/modules/util/compare.go
new file mode 100644
index 0000000000..c03a823d85
--- /dev/null
+++ b/modules/util/compare.go
@@ -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
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f43badaab2..949f560a33 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -945,11 +945,19 @@ settings.protected_branch=Branch Protection
settings.protected_branch_can_push=Allow push?
settings.protected_branch_can_push_yes=You can push
settings.protected_branch_can_push_no=You can not push
+settings.branch_protection = Branch Protection for <b>%s</b>
+settings.protect_this_branch = Protect this branch
+settings.protect_this_branch_desc = Disable force pushes and prevent deletion.
+settings.protect_whitelist_committers = Whitelist who can push to this branch
+settings.protect_whitelist_committers_desc = Add users or teams to this branch's whitelist. Whitelisted users bypass the typical push restrictions.
+settings.protect_whitelist_users = Users who can push to this branch
+settings.protect_whitelist_search_users = Search users
+settings.protect_whitelist_teams = Teams whose members can push to this branch.
+settings.protect_whitelist_search_teams = Search teams
settings.add_protected_branch=Enable protection
settings.delete_protected_branch=Disable protection
-settings.add_protected_branch_success=%s Locked successfully
-settings.add_protected_branch_failed= %s Locked failed
-settings.remove_protected_branch_success=%s Unlocked successfully
+settings.update_protect_branch_success = Branch %s protect options changed successfully.
+settings.remove_protected_branch_success= Branch %s protect options removed successfully
settings.protected_branch_deletion=To delete a protected branch
settings.protected_branch_deletion_desc=Anyone with write permissions will be able to push directly to this branch. Are you sure?
settings.default_branch_desc = The default branch is considered the "base" branch in your repository against which all pull requests and code commits are automatically made, unless you specify a different branch.
diff --git a/public/css/index.css b/public/css/index.css
index ffc7fe1a19..d09490adbe 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -2344,6 +2344,30 @@ footer .ui.language .menu {
margin-left: 5px;
margin-top: -3px;
}
+.repository.settings.branches .protected-branches .selection.dropdown {
+ width: 300px;
+}
+.repository.settings.branches .protected-branches .item {
+ border: 1px solid #eaeaea;
+ padding: 10px 15px;
+}
+.repository.settings.branches .protected-branches .item:not(:last-child) {
+ border-bottom: 0;
+}
+.repository.settings.branches .branch-protection .help {
+ margin-left: 26px;
+ padding-top: 0;
+}
+.repository.settings.branches .branch-protection .fields {
+ margin-left: 20px;
+ display: block;
+}
+.repository.settings.branches .branch-protection .whitelist {
+ margin-left: 26px;
+}
+.repository.settings.branches .branch-protection .whitelist .dropdown img {
+ display: inline-block;
+}
.repository.settings.webhook .events .column {
padding-bottom: 0;
}
diff --git a/public/js/index.js b/public/js/index.js
index dc2a09c9ed..cded5e2a1a 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -639,42 +639,18 @@ function initRepository() {
if ($('.repository.compare.pull').length > 0) {
initFilterSearchDropdown('.choose.branch .dropdown');
}
-}
-
-function initProtectedBranch() {
- $('#protectedBranch').change(function () {
- var $this = $(this);
- $.post($this.data('url'), {
- "_csrf": csrf,
- "canPush": true,
- "branchName": $this.val(),
- },
- function (data) {
- if (data.redirect) {
- window.location.href = data.redirect;
- } else {
- location.reload();
- }
- }
- );
- });
- $('.rm').click(function () {
- var $this = $(this);
- $.post($this.data('url'), {
- "_csrf": csrf,
- "canPush": false,
- "branchName": $this.data('val'),
- },
- function (data) {
- if (data.redirect) {
- window.location.href = data.redirect;
- } else {
- location.reload();
- }
+ // Branches
+ if ($('.repository.settings.branches').length > 0) {
+ initFilterSearchDropdown('.protected-branches .dropdown');
+ $('.enable-protection, .enable-whitelist').change(function () {
+ if (this.checked) {
+ $($(this).data('target')).removeClass('disabled');
+ } else {
+ $($(this).data('target')).addClass('disabled');
}
- );
- });
+ });
+ }
}
function initRepositoryCollaboration() {
@@ -1598,7 +1574,6 @@ $(document).ready(function () {
initEditForm();
initEditor();
initOrganization();
- initProtectedBranch();
initWebhook();
initAdmin();
initCodeView();
diff --git a/public/less/_repository.less b/public/less/_repository.less
index 34fdc26092..d161cc65e5 100644
--- a/public/less/_repository.less
+++ b/public/less/_repository.less
@@ -1251,6 +1251,39 @@
}
}
+ &.branches {
+ .protected-branches {
+ .selection.dropdown {
+ width: 300px;
+ }
+ .item {
+ border: 1px solid #eaeaea;
+ padding: 10px 15px;
+
+ &:not(:last-child) {
+ border-bottom: 0;
+ }
+ }
+ }
+ .branch-protection {
+ .help {
+ margin-left: 26px;
+ padding-top: 0;
+ }
+ .fields {
+ margin-left: 20px;
+ display: block;
+ }
+ .whitelist {
+ margin-left: 26px;
+
+ .dropdown img {
+ display: inline-block;
+ }
+ }
+ }
+ }
+
&.webhook {
.events {
.column {
diff --git a/routers/private/branch.go b/routers/private/branch.go
index 8e42f73039..448c61f1db 100644
--- a/routers/private/branch.go
+++ b/routers/private/branch.go
@@ -24,7 +24,29 @@ func GetProtectedBranchBy(ctx *macaron.Context) {
ctx.JSON(200, protectBranch)
} else {
ctx.JSON(200, &models.ProtectedBranch{
- CanPush: true,
+ ID: 0,
+ })
+ }
+}
+
+// CanUserPush returns if user push
+func CanUserPush(ctx *macaron.Context) {
+ pbID := ctx.ParamsInt64(":pbid")
+ userID := ctx.ParamsInt64(":userid")
+
+ protectBranch, err := models.GetProtectedBranchByID(pbID)
+ if err != nil {
+ ctx.JSON(500, map[string]interface{}{
+ "err": err.Error(),
+ })
+ return
+ } else if protectBranch != nil {
+ ctx.JSON(200, map[string]interface{}{
+ "can_push": protectBranch.CanUserPush(userID),
+ })
+ } else {
+ ctx.JSON(200, map[string]interface{}{
+ "can_push": false,
})
}
}
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 3e7233226a..b69411dd06 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -42,6 +42,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/", func() {
m.Post("/ssh/:id/update", UpdatePublicKey)
m.Post("/push/update", PushUpdate)
+ m.Get("/protectedbranch/:pbid/:userid", CanUserPush)
m.Get("/branch/:id/*", GetProtectedBranchBy)
}, CheckInternalToken)
}
diff --git a/routers/repo/editor.go b/routers/repo/editor.go
index bb0475338d..42ede9a28f 100644
--- a/routers/repo/editor.go
+++ b/routers/repo/editor.go
@@ -32,7 +32,7 @@ const (
)
func renderCommitRights(ctx *context.Context) bool {
- canCommit, err := ctx.Repo.CanCommitToBranch()
+ canCommit, err := ctx.Repo.CanCommitToBranch(ctx.User)
if err != nil {
log.Error(4, "CanCommitToBranch: %v", err)
}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 0cd4edabb6..d5d1af7e49 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -694,7 +694,7 @@ func ViewIssue(ctx *context.Context) {
log.Error(4, "GetHeadRepo: %v", err)
} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch && ctx.User.IsWriterOfRepo(pull.HeadRepo) {
// Check if branch is not protected
- if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch); err != nil {
+ if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
log.Error(4, "IsProtectedBranch: %v", err)
} else if !protected {
canDelete = true
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
index 47fcff3128..87d3bdc26d 100644
--- a/routers/repo/pull.go
+++ b/routers/repo/pull.go
@@ -841,7 +841,7 @@ func CleanUpPullRequest(ctx *context.Context) {
}
// Check if branch is not protected
- if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch); err != nil || protected {
+ if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch, ctx.User); err != nil || protected {
if err != nil {
log.Error(4, "HeadRepo.IsProtectedBranch: %v", err)
}
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 6e12c7ad6d..eb4136b07d 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -25,6 +25,7 @@ const (
tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys"
+ tplProtectedBranch base.TplName = "repo/settings/protected_branch"
)
// Settings show a repository's settings page
@@ -437,143 +438,6 @@ func DeleteCollaboration(ctx *context.Context) {
})
}
-// 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)
- case "protected_branch":
- if ctx.HasError() {
- ctx.JSON(200, map[string]string{
- "redirect": setting.AppSubURL + ctx.Req.URL.Path,
- })
- return
- }
-
- branchName := strings.ToLower(ctx.Query("branchName"))
- if len(branchName) == 0 || !ctx.Repo.GitRepo.IsBranchExist(branchName) {
- ctx.JSON(200, map[string]string{
- "redirect": setting.AppSubURL + ctx.Req.URL.Path,
- })
- return
- }
-
- canPush := ctx.QueryBool("canPush")
-
- if canPush {
- if err := ctx.Repo.Repository.AddProtectedBranch(branchName, canPush); err != nil {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_protected_branch_failed", branchName))
- ctx.JSON(200, map[string]string{
- "status": "ok",
- })
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_protected_branch_success", branchName))
- ctx.JSON(200, map[string]string{
- "redirect": setting.AppSubURL + ctx.Req.URL.Path,
- })
- } else {
- if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branchName))
- }
-
- ctx.JSON(200, map[string]interface{}{
- "status": "ok",
- })
- }
- default:
- ctx.Handle(404, "", nil)
- }
-}
-
-// ChangeProtectedBranch response for changing access of a protect branch
-func ChangeProtectedBranch(ctx *context.Context) {
- if err := ctx.Repo.Repository.ChangeProtectedBranch(
- ctx.QueryInt64("id"),
- ctx.QueryBool("canPush")); err != nil {
- log.Error(4, "ChangeProtectedBranch: %v", err)
- }
-}
-
-// DeleteProtectedBranch delete a protection for a branch of a repository
-func DeleteProtectedBranch(ctx *context.Context) {
- if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success"))
- }
-
- ctx.JSON(200, map[string]interface{}{
- "redirect": ctx.Repo.RepoLink + "/settings/branches",
- })
-}
-
// parseOwnerAndRepo get repos by owner
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
diff --git a/routers/repo/setting_protected_branch.go b/routers/repo/setting_protected_branch.go
new file mode 100644
index 0000000000..7ab8ca218e
--- /dev/null
+++ b/routers/repo/setting_protected_branch.go
@@ -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))
+ }
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 938dec1dcf..067bf6a7f6 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -433,8 +433,8 @@ func RegisterRoutes(m *macaron.Macaron) {
})
m.Group("/branches", func() {
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
- m.Post("/can_push", repo.ChangeProtectedBranch)
- m.Post("/delete", repo.DeleteProtectedBranch)
+ m.Combo("/*").Get(repo.SettingsProtectedBranch).
+ Post(bindIgnErr(auth.ProtectBranchForm{}), repo.SettingsProtectedBranchPost)
}, repo.MustBeNotBare)
m.Group("/hooks", func() {
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index 7008e2e530..b685217c87 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -39,20 +39,16 @@
<h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.protected_branch"}}
</h4>
+
<div class="ui attached table segment">
<div class="ui grid padded">
<div class="eight wide column">
<div class="ui fluid dropdown selection" tabindex="0">
- <select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch">
- {{range .LeftBranches}}
- <option value="">{{$.i18n.Tr "repo.settings.choose_branch"}}</option>
- <option value="{{.}}">{{.}}</option>
- {{end}}
- </select><i class="dropdown icon"></i>
+ <i class="dropdown icon"></i>
<div class="default text">{{.i18n.Tr "repo.settings.choose_branch"}}</div>
<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
{{range .LeftBranches}}
- <div class="item" data-value="{{.}}">{{.}}</div>
+ <a class="item" href="{{$.Repository.Link}}/settings/branches/{{.}}">{{.}}</a>
{{end}}
</div>
</div>
@@ -65,8 +61,8 @@
<tbody>
{{range .ProtectedBranches}}
<tr>
- <td><div class="ui large label">{{.BranchName}}</div></td>
- <td class="right aligned"><button class="rm ui red button" data-url="{{$.Repository.Link}}/settings/branches?action=protected_branch&id={{.ID}}" data-val="{{.BranchName}}">Delete</button></td>
+ <td><div class="ui basic label blue">{{.BranchName}}</div></td>
+ <td class="right aligned"><a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/{{.BranchName}}">Edit</a></td>
</tr>
{{else}}
<tr class="center aligned"><td>{{.i18n.Tr "repo.settings.no_protected_branch"}}</td></tr>
diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl
new file mode 100644
index 0000000000..a3a153eb3f
--- /dev/null
+++ b/templates/repo/settings/protected_branch.tmpl
@@ -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" .}} \ No newline at end of file