]> source.dussan.org Git - gitea.git/commitdiff
improve protected branch to add whitelist support (#2451)
authorLunny Xiao <xiaolunwen@gmail.com>
Thu, 14 Sep 2017 08:16:22 +0000 (16:16 +0800)
committerGitHub <noreply@github.com>
Thu, 14 Sep 2017 08:16:22 +0000 (16:16 +0800)
* 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

29 files changed:
cmd/hook.go
integrations/editor_test.go
integrations/integration_test.go
integrations/internal_test.go
models/branches.go
models/migrations/migrations.go
models/migrations/v40.go [new file with mode: 0644]
models/org.go
models/org_team.go
models/repo.go
modules/auth/repo_form.go
modules/base/tool.go
modules/context/repo.go
modules/private/branch.go
modules/util/compare.go [new file with mode: 0644]
options/locale/locale_en-US.ini
public/css/index.css
public/js/index.js
public/less/_repository.less
routers/private/branch.go
routers/private/internal.go
routers/repo/editor.go
routers/repo/issue.go
routers/repo/pull.go
routers/repo/setting.go
routers/repo/setting_protected_branch.go [new file with mode: 0644]
routers/routes/routes.go
templates/repo/settings/branches.tmpl
templates/repo/settings/protected_branch.tmpl [new file with mode: 0644]

index 06250181d3a21477fb78a1e9efb41f27569169ce..0ddbb36f901180951dbd6f20cb796d5c782a3a14 100644 (file)
@@ -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), "")
                                }
                        }
                }
index 79b6bb790aa3cc2d4f073efbb9a7234ae9e430a4..cc94edfd3f5f54d36f7f7ac8b219b982fc98eec9 100644 (file)
@@ -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 {
index d43f3977b83c68a3eeff8edb4a0fae1df039b30e..0b5d8a764ddd72d0cf5685f2393f1e68103353f5 100644 (file)
@@ -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,
index c22e951bc55f02c28bfb5b463bc5c7883b829bc7..d58b8b0b4e9ee9fe59ff4e558a9b1585054fd2ae 100644 (file)
@@ -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())
        }
 }
 
index 4461da0067b7619e7edb1d2f64891883a8032808..1c3c0d17be79a50c8e5041c695c838bc35c45c8e 100644 (file)
@@ -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
-}
index a796c6d6af73a062cccc5d2b706deb3148e02871..e7542954d7687677bbbfd01627b7326f8b835388 100644 (file)
@@ -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 (file)
index 0000000..324521e
--- /dev/null
@@ -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
+}
index d43f15f9aaae5888f74f003e0cf4d283d5ae4ace..fd81753931345a4a863d1849628ad14559a70b28 100644 (file)
@@ -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)
index bc0e12b5043d1fb417bc979854ac9f4524bb9156..acddc70b5857bd71e71b00a59a6ee0118092fc1f 100644 (file)
@@ -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)
+}
index a2e63e2af7f7e1d1718b8172516b87eeedb8dd19..8d5b3b87c717d0e36efe8eddf75a252d05de1191 100644 (file)
@@ -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.
index 4e9d2bff614c5b670678a3d9fa274a8de325752d..a6454655f7a937aa79e5d5eff02a2d967220f644 100644 (file)
@@ -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)
+}
+
 //  __      __      ___.   .__    .__            __
 // /  \    /  \ ____\_ |__ |  |__ |  |__   ____ |  | __
 // \   \/\/   // __ \| __ \|  |  \|  |  \ /  _ \|  |/ /
index 543775e0dfdc7c149f246a34f064428d5f0f4fb0..26ced075dac5603a8559edf1867f696846bc79a1 100644 (file)
@@ -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 {
index e335eafde3816863a69a3060862553248bb1ec4f..a82535a6d48555c14ad375f06dd602deacf5b1f2 100644 (file)
@@ -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
        }
index 6b3b9170bb6e31f11e3fb19c4226d0b513385766..fed66d29ffb39a2c32721965b3aef4bb348f9c80 100644 (file)
@@ -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 (file)
index 0000000..c03a823
--- /dev/null
@@ -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
+}
index f43badaab2a1b9cb58aa9db9c8439c38a2d69bc0..949f560a33e19df3b50da34b8d5df575ec6cf495 100644 (file)
@@ -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.
index ffc7fe1a19d0d784c319e747cc73a106f8a9df6e..d09490adbe96ec3f0f53c45deca53f91f5bf181d 100644 (file)
@@ -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;
 }
index dc2a09c9edf6edb1fcea8ee5fd355cd97874816e..cded5e2a1aeb9ee0f1b49393ed1ed73c52285a2d 100644 (file)
@@ -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();
index 34fdc26092174c47abec28a427a9fbd58835b5f2..d161cc65e55257f3e6ffabdc8b9e283849fe0ab8 100644 (file)
                        }
                }
 
+               &.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 {
index 8e42f730392c8c215f0177fd51647b29ecc21699..448c61f1dbba4ee253830eb49bfff2e186789a3e 100644 (file)
@@ -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,
                })
        }
 }
index 3e7233226a7234c0b2d7b913d51e5f962ae3f61e..b69411dd069defdcf84b38e19d143b135efc6849 100644 (file)
@@ -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)
 }
index bb0475338d57a4d4e56e9bdec47aeb8944b3f0d9..42ede9a28fa41fa3902377e989b2825bc95cd901 100644 (file)
@@ -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)
        }
index 0cd4edabb6ab9d420f9b9d5fabed68b4206bdd1f..d5d1af7e49338b4a664da4f4e5a5ebdee15f2397 100644 (file)
@@ -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
index 47fcff3128a59c68ae8008a39734807a500418c5..87d3bdc26db677640e2d03e3fc6ed40599948ed3 100644 (file)
@@ -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)
                }
index 6e12c7ad6d0ca74c79b67db60a5b14fc4b36a925..eb4136b07d91317b039d8fc734eff7b6e9a1ceb1 100644 (file)
@@ -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 (file)
index 0000000..7ab8ca2
--- /dev/null
@@ -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))
+       }
+}
index 938dec1dcff5c5e3704b0b26806636f17200113a..067bf6a7f6b17f56762ca96d62040143baeb642d 100644 (file)
@@ -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() {
index 7008e2e530115045239c8b560d50e6f742a8b1ab..b685217c87aaf832ae6999679b088adeb3ca8ef4 100644 (file)
                <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 (file)
index 0000000..a3a153e
--- /dev/null
@@ -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