THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

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
tags/v1.3.0-rc1
Lunny Xiao 1 year ago
parent
commit
1739e84ac0

+ 25
- 23
cmd/hook.go View File

@@ -84,9 +84,10 @@ func runHookPreReceive(c *cli.Context) error {
84 84
 	// the environment setted on serv command
85 85
 	repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
86 86
 	isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
87
-	//username := os.Getenv(models.EnvRepoUsername)
88
-	//reponame := os.Getenv(models.EnvRepoName)
89
-	//repoPath := models.RepoPath(username, reponame)
87
+	username := os.Getenv(models.EnvRepoUsername)
88
+	reponame := os.Getenv(models.EnvRepoName)
89
+	userIDStr := os.Getenv(models.EnvPusherID)
90
+	repoPath := models.RepoPath(username, reponame)
90 91
 
91 92
 	buf := bytes.NewBuffer(nil)
92 93
 	scanner := bufio.NewScanner(os.Stdin)
@@ -104,36 +105,37 @@ func runHookPreReceive(c *cli.Context) error {
104 105
 			continue
105 106
 		}
106 107
 
107
-		//oldCommitID := string(fields[0])
108
+		oldCommitID := string(fields[0])
108 109
 		newCommitID := string(fields[1])
109 110
 		refFullName := string(fields[2])
110 111
 
111
-		// FIXME: when we add feature to protected branch to deny force push, then uncomment below
112
-		/*var isForce bool
113
-		// detect force push
114
-		if git.EmptySHA != oldCommitID {
115
-			output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
116
-			if err != nil {
117
-				fail("Internal error", "Fail to detect force push: %v", err)
118
-			} else if len(output) > 0 {
119
-				isForce = true
120
-			}
121
-		}*/
122
-
123 112
 		branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
124 113
 		protectBranch, err := private.GetProtectedBranchBy(repoID, branchName)
125 114
 		if err != nil {
126 115
 			log.GitLogger.Fatal(2, "retrieve protected branches information failed")
127 116
 		}
128 117
 
129
-		if protectBranch != nil {
130
-			if !protectBranch.CanPush {
131
-				// check and deletion
132
-				if newCommitID == git.EmptySHA {
133
-					fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
134
-				} else {
118
+		if protectBranch != nil && protectBranch.IsProtected() {
119
+			// detect force push
120
+			if git.EmptySHA != oldCommitID {
121
+				output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
122
+				if err != nil {
123
+					fail("Internal error", "Fail to detect force push: %v", err)
124
+				} else if len(output) > 0 {
125
+					fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
126
+				}
127
+			}
128
+
129
+			// check and deletion
130
+			if newCommitID == git.EmptySHA {
131
+				fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
132
+			} else {
133
+				userID, _ := strconv.ParseInt(userIDStr, 10, 64)
134
+				canPush, err := private.CanUserPush(protectBranch.ID, userID)
135
+				if err != nil {
136
+					fail("Internal error", "Fail to detect user can push: %v", err)
137
+				} else if !canPush {
135 138
 					fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "")
136
-					//fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
137 139
 				}
138 140
 			}
139 141
 		}

+ 19
- 6
integrations/editor_test.go View File

@@ -43,16 +43,15 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
43 43
 
44 44
 	csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
45 45
 	// Change master branch to protected
46
-	req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches?action=protected_branch", map[string]string{
47
-		"_csrf":      csrf,
48
-		"branchName": "master",
49
-		"canPush":    "true",
46
+	req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
47
+		"_csrf":     csrf,
48
+		"protected": "on",
50 49
 	})
51
-	resp := session.MakeRequest(t, req, http.StatusOK)
50
+	resp := session.MakeRequest(t, req, http.StatusFound)
52 51
 	// Check if master branch has been locked successfully
53 52
 	flashCookie := session.GetCookie("macaron_flash")
54 53
 	assert.NotNil(t, flashCookie)
55
-	assert.EqualValues(t, flashCookie.Value, "success%3Dmaster%2BLocked%2Bsuccessfully")
54
+	assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bchanged%2Bsuccessfully.", flashCookie.Value)
56 55
 
57 56
 	// Request editor page
58 57
 	req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
@@ -74,6 +73,20 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
74 73
 	resp = session.MakeRequest(t, req, http.StatusOK)
75 74
 	// Check body for error message
76 75
 	assert.Contains(t, string(resp.Body), "Can not commit to protected branch 'master'.")
76
+
77
+	// remove the protected branch
78
+	csrf = GetCSRF(t, session, "/user2/repo1/settings/branches")
79
+	// Change master branch to protected
80
+	req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
81
+		"_csrf":     csrf,
82
+		"protected": "off",
83
+	})
84
+	resp = session.MakeRequest(t, req, http.StatusFound)
85
+	// Check if master branch has been locked successfully
86
+	flashCookie = session.GetCookie("macaron_flash")
87
+	assert.NotNil(t, flashCookie)
88
+	assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bremoved%2Bsuccessfully", flashCookie.Value)
89
+
77 90
 }
78 91
 
79 92
 func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath string) *TestResponse {

+ 1
- 1
integrations/integration_test.go View File

@@ -269,7 +269,7 @@ func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestRespo
269 269
 	mac.ServeHTTP(respWriter, req)
270 270
 	if expectedStatus != NoExpectedStatus {
271 271
 		assert.EqualValues(t, expectedStatus, respWriter.HeaderCode,
272
-			"Request URL: %s", req.URL.String())
272
+			"Request URL: %s %s", req.URL.String(), buffer.String())
273 273
 	}
274 274
 	return &TestResponse{
275 275
 		HeaderCode: respWriter.HeaderCode,

+ 1
- 1
integrations/internal_test.go View File

@@ -31,7 +31,7 @@ func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr,
31 31
 		var branch models.ProtectedBranch
32 32
 		t.Log(string(resp.Body))
33 33
 		assert.NoError(t, json.Unmarshal(resp.Body, &branch))
34
-		assert.Equal(t, canPush, branch.CanPush)
34
+		assert.Equal(t, canPush, !branch.IsProtected())
35 35
 	}
36 36
 }
37 37
 

+ 112
- 80
models/branches.go View File

@@ -8,6 +8,12 @@ import (
8 8
 	"fmt"
9 9
 	"strings"
10 10
 	"time"
11
+
12
+	"code.gitea.io/gitea/modules/base"
13
+	"code.gitea.io/gitea/modules/log"
14
+	"code.gitea.io/gitea/modules/util"
15
+
16
+	"github.com/Unknwon/com"
11 17
 )
12 18
 
13 19
 const (
@@ -17,14 +23,43 @@ const (
17 23
 
18 24
 // ProtectedBranch struct
19 25
 type ProtectedBranch struct {
20
-	ID          int64  `xorm:"pk autoincr"`
21
-	RepoID      int64  `xorm:"UNIQUE(s)"`
22
-	BranchName  string `xorm:"UNIQUE(s)"`
23
-	CanPush     bool
24
-	Created     time.Time `xorm:"-"`
25
-	CreatedUnix int64     `xorm:"created"`
26
-	Updated     time.Time `xorm:"-"`
27
-	UpdatedUnix int64     `xorm:"updated"`
26
+	ID               int64  `xorm:"pk autoincr"`
27
+	RepoID           int64  `xorm:"UNIQUE(s)"`
28
+	BranchName       string `xorm:"UNIQUE(s)"`
29
+	EnableWhitelist  bool
30
+	WhitelistUserIDs []int64   `xorm:"JSON TEXT"`
31
+	WhitelistTeamIDs []int64   `xorm:"JSON TEXT"`
32
+	Created          time.Time `xorm:"-"`
33
+	CreatedUnix      int64     `xorm:"created"`
34
+	Updated          time.Time `xorm:"-"`
35
+	UpdatedUnix      int64     `xorm:"updated"`
36
+}
37
+
38
+// IsProtected returns if the branch is protected
39
+func (protectBranch *ProtectedBranch) IsProtected() bool {
40
+	return protectBranch.ID > 0
41
+}
42
+
43
+// CanUserPush returns if some user could push to this protected branch
44
+func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool {
45
+	if !protectBranch.EnableWhitelist {
46
+		return false
47
+	}
48
+
49
+	if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
50
+		return true
51
+	}
52
+
53
+	if len(protectBranch.WhitelistTeamIDs) == 0 {
54
+		return false
55
+	}
56
+
57
+	in, err := IsUserInTeams(userID, protectBranch.WhitelistTeamIDs)
58
+	if err != nil {
59
+		log.Error(1, "IsUserInTeams:", err)
60
+		return false
61
+	}
62
+	return in
28 63
 }
29 64
 
30 65
 // GetProtectedBranchByRepoID getting protected branch by repo ID
@@ -46,6 +81,73 @@ func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, er
46 81
 	return rel, nil
47 82
 }
48 83
 
84
+// GetProtectedBranchByID getting protected branch by ID
85
+func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) {
86
+	rel := &ProtectedBranch{ID: id}
87
+	has, err := x.Get(rel)
88
+	if err != nil {
89
+		return nil, err
90
+	}
91
+	if !has {
92
+		return nil, nil
93
+	}
94
+	return rel, nil
95
+}
96
+
97
+// UpdateProtectBranch saves branch protection options of repository.
98
+// If ID is 0, it creates a new record. Otherwise, updates existing record.
99
+// This function also performs check if whitelist user and team's IDs have been changed
100
+// to avoid unnecessary whitelist delete and regenerate.
101
+func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs []int64) (err error) {
102
+	if err = repo.GetOwner(); err != nil {
103
+		return fmt.Errorf("GetOwner: %v", err)
104
+	}
105
+
106
+	hasUsersChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistUserIDs, whitelistUserIDs)
107
+	if hasUsersChanged {
108
+		protectBranch.WhitelistUserIDs = make([]int64, 0, len(whitelistUserIDs))
109
+		for _, userID := range whitelistUserIDs {
110
+			has, err := hasAccess(x, userID, repo, AccessModeWrite)
111
+			if err != nil {
112
+				return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
113
+			} else if !has {
114
+				continue // Drop invalid user ID
115
+			}
116
+
117
+			protectBranch.WhitelistUserIDs = append(protectBranch.WhitelistUserIDs, userID)
118
+		}
119
+	}
120
+
121
+	// if the repo is in an orgniziation
122
+	hasTeamsChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistTeamIDs, whitelistTeamIDs)
123
+	if hasTeamsChanged {
124
+		teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite)
125
+		if err != nil {
126
+			return fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
127
+		}
128
+		protectBranch.WhitelistTeamIDs = make([]int64, 0, len(teams))
129
+		for i := range teams {
130
+			if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(whitelistTeamIDs, teams[i].ID) {
131
+				protectBranch.WhitelistTeamIDs = append(protectBranch.WhitelistTeamIDs, teams[i].ID)
132
+			}
133
+		}
134
+	}
135
+
136
+	// Make sure protectBranch.ID is not 0 for whitelists
137
+	if protectBranch.ID == 0 {
138
+		if _, err = x.Insert(protectBranch); err != nil {
139
+			return fmt.Errorf("Insert: %v", err)
140
+		}
141
+		return nil
142
+	}
143
+
144
+	if _, err = x.Id(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
145
+		return fmt.Errorf("Update: %v", err)
146
+	}
147
+
148
+	return nil
149
+}
150
+
49 151
 // GetProtectedBranches get all protected branches
50 152
 func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
51 153
 	protectedBranches := make([]*ProtectedBranch, 0)
@@ -53,7 +155,7 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
53 155
 }
54 156
 
55 157
 // IsProtectedBranch checks if branch is protected
56
-func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) {
158
+func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) {
57 159
 	protectedBranch := &ProtectedBranch{
58 160
 		RepoID:     repo.ID,
59 161
 		BranchName: branchName,
@@ -63,70 +165,12 @@ func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) {
63 165
 	if err != nil {
64 166
 		return true, err
65 167
 	} else if has {
66
-		return true, nil
168
+		return !protectedBranch.CanUserPush(doer.ID), nil
67 169
 	}
68 170
 
69 171
 	return false, nil
70 172
 }
71 173
 
72
-// AddProtectedBranch add protection to branch
73
-func (repo *Repository) AddProtectedBranch(branchName string, canPush bool) error {
74
-	protectedBranch := &ProtectedBranch{
75
-		RepoID:     repo.ID,
76
-		BranchName: branchName,
77
-	}
78
-
79
-	has, err := x.Get(protectedBranch)
80
-	if err != nil {
81
-		return err
82
-	} else if has {
83
-		return nil
84
-	}
85
-
86
-	sess := x.NewSession()
87
-	defer sess.Close()
88
-	if err = sess.Begin(); err != nil {
89
-		return err
90
-	}
91
-	protectedBranch.CanPush = canPush
92
-	if _, err = sess.InsertOne(protectedBranch); err != nil {
93
-		return err
94
-	}
95
-
96
-	return sess.Commit()
97
-}
98
-
99
-// ChangeProtectedBranch access mode sets new access mode for the ProtectedBranch.
100
-func (repo *Repository) ChangeProtectedBranch(id int64, canPush bool) error {
101
-	ProtectedBranch := &ProtectedBranch{
102
-		RepoID: repo.ID,
103
-		ID:     id,
104
-	}
105
-	has, err := x.Get(ProtectedBranch)
106
-	if err != nil {
107
-		return fmt.Errorf("get ProtectedBranch: %v", err)
108
-	} else if !has {
109
-		return nil
110
-	}
111
-
112
-	if ProtectedBranch.CanPush == canPush {
113
-		return nil
114
-	}
115
-	ProtectedBranch.CanPush = canPush
116
-
117
-	sess := x.NewSession()
118
-	defer sess.Close()
119
-	if err = sess.Begin(); err != nil {
120
-		return err
121
-	}
122
-
123
-	if _, err = sess.Id(ProtectedBranch.ID).AllCols().Update(ProtectedBranch); err != nil {
124
-		return fmt.Errorf("update ProtectedBranch: %v", err)
125
-	}
126
-
127
-	return sess.Commit()
128
-}
129
-
130 174
 // DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
131 175
 func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
132 176
 	protectedBranch := &ProtectedBranch{
@@ -148,15 +192,3 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
148 192
 
149 193
 	return sess.Commit()
150 194
 }
151
-
152
-// newProtectedBranch insert one queue
153
-func newProtectedBranch(protectedBranch *ProtectedBranch) error {
154
-	_, err := x.InsertOne(protectedBranch)
155
-	return err
156
-}
157
-
158
-// UpdateProtectedBranch update queue
159
-func UpdateProtectedBranch(protectedBranch *ProtectedBranch) error {
160
-	_, err := x.Update(protectedBranch)
161
-	return err
162
-}

+ 2
- 0
models/migrations/migrations.go View File

@@ -128,6 +128,8 @@ var migrations = []Migration{
128 128
 	NewMigration("remove commits and settings unit types", removeCommitsUnitType),
129 129
 	// v39 -> v40
130 130
 	NewMigration("adds time tracking and stopwatches", addTimetracking),
131
+	// v40 -> v41
132
+	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
131 133
 }
132 134
 
133 135
 // Migrate database to current version

+ 55
- 0
models/migrations/v40.go View File

@@ -0,0 +1,55 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package migrations
6
+
7
+import (
8
+	"fmt"
9
+	"time"
10
+
11
+	"code.gitea.io/gitea/modules/log"
12
+	"code.gitea.io/gitea/modules/setting"
13
+
14
+	"github.com/go-xorm/xorm"
15
+)
16
+
17
+func migrateProtectedBranchStruct(x *xorm.Engine) error {
18
+	type ProtectedBranch struct {
19
+		ID          int64  `xorm:"pk autoincr"`
20
+		RepoID      int64  `xorm:"UNIQUE(s)"`
21
+		BranchName  string `xorm:"UNIQUE(s)"`
22
+		CanPush     bool
23
+		Created     time.Time `xorm:"-"`
24
+		CreatedUnix int64
25
+		Updated     time.Time `xorm:"-"`
26
+		UpdatedUnix int64
27
+	}
28
+
29
+	var pbs []ProtectedBranch
30
+	err := x.Find(&pbs)
31
+	if err != nil {
32
+		return err
33
+	}
34
+
35
+	for _, pb := range pbs {
36
+		if pb.CanPush {
37
+			if _, err = x.ID(pb.ID).Delete(new(ProtectedBranch)); err != nil {
38
+				return err
39
+			}
40
+		}
41
+	}
42
+
43
+	switch {
44
+	case setting.UseSQLite3:
45
+		log.Warn("Unable to drop columns in SQLite")
46
+	case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB:
47
+		if _, err := x.Exec("ALTER TABLE protected_branch DROP COLUMN can_push"); err != nil {
48
+			return fmt.Errorf("DROP COLUMN can_push: %v", err)
49
+		}
50
+	default:
51
+		log.Fatal(4, "Unrecognized DB")
52
+	}
53
+
54
+	return nil
55
+}

+ 5
- 0
models/org.go View File

@@ -577,6 +577,11 @@ func (org *User) getUserTeamIDs(e Engine, userID int64) ([]int64, error) {
577 577
 		Find(&teamIDs)
578 578
 }
579 579
 
580
+// TeamsWithAccessToRepo returns all teamsthat have given access level to the repository.
581
+func (org *User) TeamsWithAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
582
+	return GetTeamsWithAccessToRepo(org.ID, repoID, mode)
583
+}
584
+
580 585
 // GetUserTeamIDs returns of all team IDs of the organization that user is member of.
581 586
 func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) {
582 587
 	return org.getUserTeamIDs(x, userID)

+ 20
- 0
models/org_team.go View File

@@ -35,6 +35,11 @@ func (t *Team) GetUnitTypes() []UnitType {
35 35
 	return t.UnitTypes
36 36
 }
37 37
 
38
+// HasWriteAccess returns true if team has at least write level access mode.
39
+func (t *Team) HasWriteAccess() bool {
40
+	return t.Authorize >= AccessModeWrite
41
+}
42
+
38 43
 // IsOwnerTeam returns true if team is owner team.
39 44
 func (t *Team) IsOwnerTeam() bool {
40 45
 	return t.Name == ownerTeamName
@@ -594,6 +599,11 @@ func RemoveTeamMember(team *Team, userID int64) error {
594 599
 	return sess.Commit()
595 600
 }
596 601
 
602
+// IsUserInTeams returns if a user in some teams
603
+func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) {
604
+	return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
605
+}
606
+
597 607
 // ___________                  __________
598 608
 // \__    ___/___ _____    _____\______   \ ____ ______   ____
599 609
 //   |    |_/ __ \\__  \  /     \|       _// __ \\____ \ /  _ \
@@ -639,3 +649,13 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error {
639 649
 	})
640 650
 	return err
641 651
 }
652
+
653
+// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
654
+func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
655
+	teams := make([]*Team, 0, 5)
656
+	return teams, x.Where("team.authorize >= ?", mode).
657
+		Join("INNER", "team_repo", "team_repo.team_id = team.id").
658
+		And("team_repo.org_id = ?", orgID).
659
+		And("team_repo.repo_id = ?", repoID).
660
+		Find(&teams)
661
+}

+ 36
- 0
models/repo.go View File

@@ -656,6 +656,42 @@ func (repo *Repository) CanEnableEditor() bool {
656 656
 	return !repo.IsMirror
657 657
 }
658 658
 
659
+// GetWriters returns all users that have write access to the repository.
660
+func (repo *Repository) GetWriters() (_ []*User, err error) {
661
+	return repo.getUsersWithAccessMode(x, AccessModeWrite)
662
+}
663
+
664
+// getUsersWithAccessMode returns users that have at least given access mode to the repository.
665
+func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []*User, err error) {
666
+	if err = repo.getOwner(e); err != nil {
667
+		return nil, err
668
+	}
669
+
670
+	accesses := make([]*Access, 0, 10)
671
+	if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
672
+		return nil, err
673
+	}
674
+
675
+	// Leave a seat for owner itself to append later, but if owner is an organization
676
+	// and just waste 1 unit is cheaper than re-allocate memory once.
677
+	users := make([]*User, 0, len(accesses)+1)
678
+	if len(accesses) > 0 {
679
+		userIDs := make([]int64, len(accesses))
680
+		for i := 0; i < len(accesses); i++ {
681
+			userIDs[i] = accesses[i].UserID
682
+		}
683
+
684
+		if err = e.In("id", userIDs).Find(&users); err != nil {
685
+			return nil, err
686
+		}
687
+	}
688
+	if !repo.Owner.IsOrganization() {
689
+		users = append(users, repo.Owner)
690
+	}
691
+
692
+	return users, nil
693
+}
694
+
659 695
 // NextIssueIndex returns the next issue index
660 696
 // FIXME: should have a mutex to prevent producing same index for two issues that are created
661 697
 // closely enough.

+ 20
- 0
modules/auth/repo_form.go View File

@@ -113,6 +113,26 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
113 113
 	return validate(errs, ctx.Data, f, ctx.Locale)
114 114
 }
115 115
 
116
+// __________                             .__
117
+// \______   \____________    ____   ____ |  |__
118
+//  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
119
+//  |    |   \ |  | \// __ \|   |  \  \___|   Y  \
120
+//  |______  / |__|  (____  /___|  /\___  >___|  /
121
+//         \/             \/     \/     \/     \/
122
+
123
+// ProtectBranchForm form for changing protected branch settings
124
+type ProtectBranchForm struct {
125
+	Protected       bool
126
+	EnableWhitelist bool
127
+	WhitelistUsers  string
128
+	WhitelistTeams  string
129
+}
130
+
131
+// Validate validates the fields
132
+func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
133
+	return validate(errs, ctx.Data, f, ctx.Locale)
134
+}
135
+
116 136
 //  __      __      ___.   .__    .__            __
117 137
 // /  \    /  \ ____\_ |__ |  |__ |  |__   ____ |  | __
118 138
 // \   \/\/   // __ \| __ \|  |  \|  |  \ /  _ \|  |/ /

+ 10
- 0
modules/base/tool.go View File

@@ -497,6 +497,16 @@ func Int64sToMap(ints []int64) map[int64]bool {
497 497
 	return m
498 498
 }
499 499
 
500
+// Int64sContains returns if a int64 in a slice of int64
501
+func Int64sContains(intsSlice []int64, a int64) bool {
502
+	for _, c := range intsSlice {
503
+		if c == a {
504
+			return true
505
+		}
506
+	}
507
+	return false
508
+}
509
+
500 510
 // IsLetter reports whether the rune is a letter (category L).
501 511
 // https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257
502 512
 func IsLetter(ch rune) bool {

+ 2
- 2
modules/context/repo.go View File

@@ -78,8 +78,8 @@ func (r *Repository) CanEnableEditor() bool {
78 78
 
79 79
 // CanCommitToBranch returns true if repository is editable and user has proper access level
80 80
 //   and branch is not protected
81
-func (r *Repository) CanCommitToBranch() (bool, error) {
82
-	protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName)
81
+func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) {
82
+	protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName, doer)
83 83
 	if err != nil {
84 84
 		return false, err
85 85
 	}

+ 26
- 0
modules/private/branch.go View File

@@ -38,3 +38,29 @@ func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBra
38 38
 
39 39
 	return &branch, nil
40 40
 }
41
+
42
+// CanUserPush returns if user can push
43
+func CanUserPush(protectedBranchID, userID int64) (bool, error) {
44
+	// Ask for running deliver hook and test pull request tasks.
45
+	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranch/%d/%d", protectedBranchID, userID)
46
+	log.GitLogger.Trace("CanUserPush: %s", reqURL)
47
+
48
+	resp, err := newInternalRequest(reqURL, "GET").Response()
49
+	if err != nil {
50
+		return false, err
51
+	}
52
+
53
+	var canPush = make(map[string]interface{})
54
+	if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil {
55
+		return false, err
56
+	}
57
+
58
+	defer resp.Body.Close()
59
+
60
+	// All 2XX status codes are accepted and others will return an error
61
+	if resp.StatusCode/100 != 2 {
62
+		return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err)
63
+	}
64
+
65
+	return canPush["can_push"].(bool), nil
66
+}

+ 29
- 0
modules/util/compare.go View File

@@ -0,0 +1,29 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package util
6
+
7
+import "sort"
8
+
9
+// Int64Slice attaches the methods of Interface to []int64, sorting in increasing order.
10
+type Int64Slice []int64
11
+
12
+func (p Int64Slice) Len() int           { return len(p) }
13
+func (p Int64Slice) Less(i, j int) bool { return p[i] < p[j] }
14
+func (p Int64Slice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
15
+
16
+// IsSliceInt64Eq returns if the two slice has the same elements but different sequences.
17
+func IsSliceInt64Eq(a, b []int64) bool {
18
+	if len(a) != len(b) {
19
+		return false
20
+	}
21
+	sort.Sort(Int64Slice(a))
22
+	sort.Sort(Int64Slice(b))
23
+	for i := 0; i < len(a); i++ {
24
+		if a[i] != b[i] {
25
+			return false
26
+		}
27
+	}
28
+	return true
29
+}

+ 11
- 3
options/locale/locale_en-US.ini View File

@@ -945,11 +945,19 @@ settings.protected_branch=Branch Protection
945 945
 settings.protected_branch_can_push=Allow push?
946 946
 settings.protected_branch_can_push_yes=You can push
947 947
 settings.protected_branch_can_push_no=You can not push
948
+settings.branch_protection = Branch Protection for <b>%s</b>
949
+settings.protect_this_branch = Protect this branch
950
+settings.protect_this_branch_desc = Disable force pushes and prevent deletion.
951
+settings.protect_whitelist_committers = Whitelist who can push to this branch
952
+settings.protect_whitelist_committers_desc = Add users or teams to this branch's whitelist. Whitelisted users bypass the typical push restrictions.
953
+settings.protect_whitelist_users = Users who can push to this branch
954
+settings.protect_whitelist_search_users = Search users
955
+settings.protect_whitelist_teams = Teams whose members can push to this branch.
956
+settings.protect_whitelist_search_teams = Search teams
948 957
 settings.add_protected_branch=Enable protection
949 958
 settings.delete_protected_branch=Disable protection
950
-settings.add_protected_branch_success=%s Locked successfully
951
-settings.add_protected_branch_failed= %s Locked failed
952
-settings.remove_protected_branch_success=%s Unlocked successfully
959
+settings.update_protect_branch_success = Branch %s protect options changed successfully.
960
+settings.remove_protected_branch_success= Branch %s protect options removed successfully
953 961
 settings.protected_branch_deletion=To delete a protected branch
954 962
 settings.protected_branch_deletion_desc=Anyone with write permissions will be able to push directly to this branch. Are you sure?
955 963
 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.

+ 24
- 0
public/css/index.css View File

@@ -2344,6 +2344,30 @@ footer .ui.language .menu {
2344 2344
   margin-left: 5px;
2345 2345
   margin-top: -3px;
2346 2346
 }
2347
+.repository.settings.branches .protected-branches .selection.dropdown {
2348
+  width: 300px;
2349
+}
2350
+.repository.settings.branches .protected-branches .item {
2351
+  border: 1px solid #eaeaea;
2352
+  padding: 10px 15px;
2353
+}
2354
+.repository.settings.branches .protected-branches .item:not(:last-child) {
2355
+  border-bottom: 0;
2356
+}
2357
+.repository.settings.branches .branch-protection .help {
2358
+  margin-left: 26px;
2359
+  padding-top: 0;
2360
+}
2361
+.repository.settings.branches .branch-protection .fields {
2362
+  margin-left: 20px;
2363
+  display: block;
2364
+}
2365
+.repository.settings.branches .branch-protection .whitelist {
2366
+  margin-left: 26px;
2367
+}
2368
+.repository.settings.branches .branch-protection .whitelist .dropdown img {
2369
+  display: inline-block;
2370
+}
2347 2371
 .repository.settings.webhook .events .column {
2348 2372
   padding-bottom: 0;
2349 2373
 }

+ 10
- 35
public/js/index.js View File

@@ -639,42 +639,18 @@ function initRepository() {
639 639
     if ($('.repository.compare.pull').length > 0) {
640 640
         initFilterSearchDropdown('.choose.branch .dropdown');
641 641
     }
642
-}
643
-
644
-function initProtectedBranch() {
645
-    $('#protectedBranch').change(function () {
646
-        var $this = $(this);
647
-        $.post($this.data('url'), {
648
-                "_csrf": csrf,
649
-                "canPush": true,
650
-                "branchName": $this.val(),
651
-            },
652
-            function (data) {
653
-                if (data.redirect) {
654
-                    window.location.href = data.redirect;
655
-                } else {
656
-                    location.reload();
657
-                }
658
-            }
659
-        );
660
-    });
661 642
 
662
-    $('.rm').click(function () {
663
-        var $this = $(this);
664
-        $.post($this.data('url'), {
665
-                "_csrf": csrf,
666
-                "canPush": false,
667
-                "branchName": $this.data('val'),
668
-            },
669
-            function (data) {
670
-                if (data.redirect) {
671
-                    window.location.href = data.redirect;
672
-                } else {
673
-                    location.reload();
674
-                }
643
+    // Branches
644
+    if ($('.repository.settings.branches').length > 0) {
645
+        initFilterSearchDropdown('.protected-branches .dropdown');
646
+        $('.enable-protection, .enable-whitelist').change(function () {
647
+            if (this.checked) {
648
+                $($(this).data('target')).removeClass('disabled');
649
+            } else {
650
+                $($(this).data('target')).addClass('disabled');
675 651
             }
676
-        );
677
-    });
652
+        });
653
+    }
678 654
 }
679 655
 
680 656
 function initRepositoryCollaboration() {
@@ -1598,7 +1574,6 @@ $(document).ready(function () {
1598 1574
     initEditForm();
1599 1575
     initEditor();
1600 1576
     initOrganization();
1601
-    initProtectedBranch();
1602 1577
     initWebhook();
1603 1578
     initAdmin();
1604 1579
     initCodeView();

+ 33
- 0
public/less/_repository.less View File

@@ -1251,6 +1251,39 @@
1251 1251
 			}
1252 1252
 		}
1253 1253
 
1254
+		&.branches {
1255
+			.protected-branches {
1256
+				.selection.dropdown {
1257
+					width: 300px;
1258
+				}
1259
+				.item {
1260
+			    border: 1px solid #eaeaea;
1261
+			    padding: 10px 15px;
1262
+
1263
+			    &:not(:last-child) {
1264
+				    border-bottom: 0;
1265
+			    }
1266
+				}
1267
+			}
1268
+			.branch-protection {
1269
+				.help {
1270
+					margin-left: 26px;
1271
+					padding-top: 0;
1272
+				}
1273
+				.fields {
1274
+					margin-left: 20px;
1275
+					display: block;
1276
+				}
1277
+				.whitelist {
1278
+					margin-left: 26px;
1279
+
1280
+					.dropdown img {
1281
+						display: inline-block;
1282
+					}
1283
+				}
1284
+			}
1285
+		}
1286
+
1254 1287
 		&.webhook {
1255 1288
 			.events {
1256 1289
 				.column {

+ 23
- 1
routers/private/branch.go View File

@@ -24,7 +24,29 @@ func GetProtectedBranchBy(ctx *macaron.Context) {
24 24
 		ctx.JSON(200, protectBranch)
25 25
 	} else {
26 26
 		ctx.JSON(200, &models.ProtectedBranch{
27
-			CanPush: true,
27
+			ID: 0,
28
+		})
29
+	}
30
+}
31
+
32
+// CanUserPush returns if user push
33
+func CanUserPush(ctx *macaron.Context) {
34
+	pbID := ctx.ParamsInt64(":pbid")
35
+	userID := ctx.ParamsInt64(":userid")
36
+
37
+	protectBranch, err := models.GetProtectedBranchByID(pbID)
38
+	if err != nil {
39
+		ctx.JSON(500, map[string]interface{}{
40
+			"err": err.Error(),
41
+		})
42
+		return
43
+	} else if protectBranch != nil {
44
+		ctx.JSON(200, map[string]interface{}{
45
+			"can_push": protectBranch.CanUserPush(userID),
46
+		})
47
+	} else {
48
+		ctx.JSON(200, map[string]interface{}{
49
+			"can_push": false,
28 50
 		})
29 51
 	}
30 52
 }

+ 1
- 0
routers/private/internal.go View File

@@ -42,6 +42,7 @@ func RegisterRoutes(m *macaron.Macaron) {
42 42
 	m.Group("/", func() {
43 43
 		m.Post("/ssh/:id/update", UpdatePublicKey)
44 44
 		m.Post("/push/update", PushUpdate)
45
+		m.Get("/protectedbranch/:pbid/:userid", CanUserPush)
45 46
 		m.Get("/branch/:id/*", GetProtectedBranchBy)
46 47
 	}, CheckInternalToken)
47 48
 }

+ 1
- 1
routers/repo/editor.go View File

@@ -32,7 +32,7 @@ const (
32 32
 )
33 33
 
34 34
 func renderCommitRights(ctx *context.Context) bool {
35
-	canCommit, err := ctx.Repo.CanCommitToBranch()
35
+	canCommit, err := ctx.Repo.CanCommitToBranch(ctx.User)
36 36
 	if err != nil {
37 37
 		log.Error(4, "CanCommitToBranch: %v", err)
38 38
 	}

+ 1
- 1
routers/repo/issue.go View File

@@ -694,7 +694,7 @@ func ViewIssue(ctx *context.Context) {
694 694
 				log.Error(4, "GetHeadRepo: %v", err)
695 695
 			} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch && ctx.User.IsWriterOfRepo(pull.HeadRepo) {
696 696
 				// Check if branch is not protected
697
-				if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch); err != nil {
697
+				if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
698 698
 					log.Error(4, "IsProtectedBranch: %v", err)
699 699
 				} else if !protected {
700 700
 					canDelete = true

+ 1
- 1
routers/repo/pull.go View File

@@ -841,7 +841,7 @@ func CleanUpPullRequest(ctx *context.Context) {
841 841
 	}
842 842
 
843 843
 	// Check if branch is not protected
844
-	if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch); err != nil || protected {
844
+	if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch, ctx.User); err != nil || protected {
845 845
 		if err != nil {
846 846
 			log.Error(4, "HeadRepo.IsProtectedBranch: %v", err)
847 847
 		}

+ 1
- 137
routers/repo/setting.go View File

@@ -25,6 +25,7 @@ const (
25 25
 	tplGithooks        base.TplName = "repo/settings/githooks"
26 26
 	tplGithookEdit     base.TplName = "repo/settings/githook_edit"
27 27
 	tplDeployKeys      base.TplName = "repo/settings/deploy_keys"
28
+	tplProtectedBranch base.TplName = "repo/settings/protected_branch"
28 29
 )
29 30
 
30 31
 // Settings show a repository's settings page
@@ -437,143 +438,6 @@ func DeleteCollaboration(ctx *context.Context) {
437 438
 	})
438 439
 }
439 440
 
440
-// ProtectedBranch render the page to protect the repository
441
-func ProtectedBranch(ctx *context.Context) {
442
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
443
-	ctx.Data["PageIsSettingsBranches"] = true
444
-
445
-	protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
446
-	if err != nil {
447
-		ctx.Handle(500, "GetProtectedBranches", err)
448
-		return
449
-	}
450
-	ctx.Data["ProtectedBranches"] = protectedBranches
451
-
452
-	branches := ctx.Data["Branches"].([]string)
453
-	leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
454
-	for _, b := range branches {
455
-		var protected bool
456
-		for _, pb := range protectedBranches {
457
-			if b == pb.BranchName {
458
-				protected = true
459
-				break
460
-			}
461
-		}
462
-		if !protected {
463
-			leftBranches = append(leftBranches, b)
464
-		}
465
-	}
466
-
467
-	ctx.Data["LeftBranches"] = leftBranches
468
-
469
-	ctx.HTML(200, tplBranches)
470
-}
471
-
472
-// ProtectedBranchPost response for protect for a branch of a repository
473
-func ProtectedBranchPost(ctx *context.Context) {
474
-	ctx.Data["Title"] = ctx.Tr("repo.settings")
475
-	ctx.Data["PageIsSettingsBranches"] = true
476
-
477
-	repo := ctx.Repo.Repository
478
-
479
-	switch ctx.Query("action") {
480
-	case "default_branch":
481
-		if ctx.HasError() {
482
-			ctx.HTML(200, tplBranches)
483
-			return
484
-		}
485
-
486
-		branch := ctx.Query("branch")
487
-		if !ctx.Repo.GitRepo.IsBranchExist(branch) {
488
-			ctx.Status(404)
489
-			return
490
-		} else if repo.DefaultBranch != branch {
491
-			repo.DefaultBranch = branch
492
-			if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
493
-				if !git.IsErrUnsupportedVersion(err) {
494
-					ctx.Handle(500, "SetDefaultBranch", err)
495
-					return
496
-				}
497
-			}
498
-			if err := repo.UpdateDefaultBranch(); err != nil {
499
-				ctx.Handle(500, "SetDefaultBranch", err)
500
-				return
501
-			}
502
-		}
503
-
504
-		log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
505
-
506
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
507
-		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
508
-	case "protected_branch":
509
-		if ctx.HasError() {
510
-			ctx.JSON(200, map[string]string{
511
-				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
512
-			})
513
-			return
514
-		}
515
-
516
-		branchName := strings.ToLower(ctx.Query("branchName"))
517
-		if len(branchName) == 0 || !ctx.Repo.GitRepo.IsBranchExist(branchName) {
518
-			ctx.JSON(200, map[string]string{
519
-				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
520
-			})
521
-			return
522
-		}
523
-
524
-		canPush := ctx.QueryBool("canPush")
525
-
526
-		if canPush {
527
-			if err := ctx.Repo.Repository.AddProtectedBranch(branchName, canPush); err != nil {
528
-				ctx.Flash.Error(ctx.Tr("repo.settings.add_protected_branch_failed", branchName))
529
-				ctx.JSON(200, map[string]string{
530
-					"status": "ok",
531
-				})
532
-				return
533
-			}
534
-
535
-			ctx.Flash.Success(ctx.Tr("repo.settings.add_protected_branch_success", branchName))
536
-			ctx.JSON(200, map[string]string{
537
-				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
538
-			})
539
-		} else {
540
-			if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
541
-				ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
542
-			} else {
543
-				ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branchName))
544
-			}
545
-
546
-			ctx.JSON(200, map[string]interface{}{
547
-				"status": "ok",
548
-			})
549
-		}
550
-	default:
551
-		ctx.Handle(404, "", nil)
552
-	}
553
-}
554
-
555
-// ChangeProtectedBranch response for changing access of a protect branch
556
-func ChangeProtectedBranch(ctx *context.Context) {
557
-	if err := ctx.Repo.Repository.ChangeProtectedBranch(
558
-		ctx.QueryInt64("id"),
559
-		ctx.QueryBool("canPush")); err != nil {
560
-		log.Error(4, "ChangeProtectedBranch: %v", err)
561
-	}
562
-}
563
-
564
-// DeleteProtectedBranch delete a protection for a branch of a repository
565
-func DeleteProtectedBranch(ctx *context.Context) {
566
-	if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
567
-		ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
568
-	} else {
569
-		ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success"))
570
-	}
571
-
572
-	ctx.JSON(200, map[string]interface{}{
573
-		"redirect": ctx.Repo.RepoLink + "/settings/branches",
574
-	})
575
-}
576
-
577 441
 // parseOwnerAndRepo get repos by owner
578 442
 func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
579 443
 	owner, err := models.GetUserByName(ctx.Params(":username"))

+ 186
- 0
routers/repo/setting_protected_branch.go View File

@@ -0,0 +1,186 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package repo
6
+
7
+import (
8
+	"fmt"
9
+	"strings"
10
+
11
+	"code.gitea.io/git"
12
+	"code.gitea.io/gitea/models"
13
+	"code.gitea.io/gitea/modules/auth"
14
+	"code.gitea.io/gitea/modules/base"
15
+	"code.gitea.io/gitea/modules/context"
16
+	"code.gitea.io/gitea/modules/log"
17
+	"code.gitea.io/gitea/modules/setting"
18
+)
19
+
20
+// ProtectedBranch render the page to protect the repository
21
+func ProtectedBranch(ctx *context.Context) {
22
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
23
+	ctx.Data["PageIsSettingsBranches"] = true
24
+
25
+	protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
26
+	if err != nil {
27
+		ctx.Handle(500, "GetProtectedBranches", err)
28
+		return
29
+	}
30
+	ctx.Data["ProtectedBranches"] = protectedBranches
31
+
32
+	branches := ctx.Data["Branches"].([]string)
33
+	leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
34
+	for _, b := range branches {
35
+		var protected bool
36
+		for _, pb := range protectedBranches {
37
+			if b == pb.BranchName {
38
+				protected = true
39
+				break
40
+			}
41
+		}
42
+		if !protected {
43
+			leftBranches = append(leftBranches, b)
44
+		}
45
+	}
46
+
47
+	ctx.Data["LeftBranches"] = leftBranches
48
+
49
+	ctx.HTML(200, tplBranches)
50
+}
51
+
52
+// ProtectedBranchPost response for protect for a branch of a repository
53
+func ProtectedBranchPost(ctx *context.Context) {
54
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
55
+	ctx.Data["PageIsSettingsBranches"] = true
56
+
57
+	repo := ctx.Repo.Repository
58
+
59
+	switch ctx.Query("action") {
60
+	case "default_branch":
61
+		if ctx.HasError() {
62
+			ctx.HTML(200, tplBranches)
63
+			return
64
+		}
65
+
66
+		branch := ctx.Query("branch")
67
+		if !ctx.Repo.GitRepo.IsBranchExist(branch) {
68
+			ctx.Status(404)
69
+			return
70
+		} else if repo.DefaultBranch != branch {
71
+			repo.DefaultBranch = branch
72
+			if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
73
+				if !git.IsErrUnsupportedVersion(err) {
74
+					ctx.Handle(500, "SetDefaultBranch", err)
75
+					return
76
+				}
77
+			}
78
+			if err := repo.UpdateDefaultBranch(); err != nil {
79
+				ctx.Handle(500, "SetDefaultBranch", err)
80
+				return
81
+			}
82
+		}
83
+
84
+		log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
85
+
86
+		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
87
+		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
88
+	default:
89
+		ctx.Handle(404, "", nil)
90
+	}
91
+}
92
+
93
+// SettingsProtectedBranch renders the protected branch setting page
94
+func SettingsProtectedBranch(c *context.Context) {
95
+	branch := c.Params("*")
96
+	if !c.Repo.GitRepo.IsBranchExist(branch) {
97
+		c.NotFound()
98
+		return
99
+	}
100
+
101
+	c.Data["Title"] = c.Tr("repo.settings.protected_branches") + " - " + branch
102
+	c.Data["PageIsSettingsBranches"] = true
103
+
104
+	protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
105
+	if err != nil {
106
+		if !models.IsErrBranchNotExist(err) {
107
+			c.Handle(500, "GetProtectBranchOfRepoByName", err)
108
+			return
109
+		}
110
+	}
111
+
112
+	if protectBranch == nil {
113
+		// No options found, create defaults.
114
+		protectBranch = &models.ProtectedBranch{
115
+			BranchName: branch,
116
+		}
117
+	}
118
+
119
+	users, err := c.Repo.Repository.GetWriters()
120
+	if err != nil {
121
+		c.Handle(500, "Repo.Repository.GetWriters", err)
122
+		return
123
+	}
124
+	c.Data["Users"] = users
125
+	c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
126
+
127
+	if c.Repo.Owner.IsOrganization() {
128
+		teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite)
129
+		if err != nil {
130
+			c.Handle(500, "Repo.Owner.TeamsWithAccessToRepo", err)
131
+			return
132
+		}
133
+		c.Data["Teams"] = teams
134
+		c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
135
+	}
136
+
137
+	c.Data["Branch"] = protectBranch
138
+	c.HTML(200, tplProtectedBranch)
139
+}
140
+
141
+// SettingsProtectedBranchPost updates the protected branch settings
142
+func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm) {
143
+	branch := ctx.Params("*")
144
+	if !ctx.Repo.GitRepo.IsBranchExist(branch) {
145
+		ctx.NotFound()
146
+		return
147
+	}
148
+
149
+	protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
150
+	if err != nil {
151
+		if !models.IsErrBranchNotExist(err) {
152
+			ctx.Handle(500, "GetProtectBranchOfRepoByName", err)
153
+			return
154
+		}
155
+	}
156
+
157
+	if f.Protected {
158
+		if protectBranch == nil {
159
+			// No options found, create defaults.
160
+			protectBranch = &models.ProtectedBranch{
161
+				RepoID:     ctx.Repo.Repository.ID,
162
+				BranchName: branch,
163
+			}
164
+		}
165
+
166
+		protectBranch.EnableWhitelist = f.EnableWhitelist
167
+		whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
168
+		whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
169
+		err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams)
170
+		if err != nil {
171
+			ctx.Handle(500, "UpdateProtectBranch", err)
172
+			return
173
+		}
174
+		ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
175
+		ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
176
+	} else {
177
+		if protectBranch != nil {
178
+			if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil {
179
+				ctx.Handle(500, "DeleteProtectedBranch", err)
180
+				return
181
+			}
182
+		}
183
+		ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
184
+		ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
185
+	}
186
+}

+ 2
- 2
routers/routes/routes.go View File

@@ -433,8 +433,8 @@ func RegisterRoutes(m *macaron.Macaron) {
433 433
 			})
434 434
 			m.Group("/branches", func() {
435 435
 				m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
436
-				m.Post("/can_push", repo.ChangeProtectedBranch)
437
-				m.Post("/delete", repo.DeleteProtectedBranch)
436
+				m.Combo("/*").Get(repo.SettingsProtectedBranch).
437
+					Post(bindIgnErr(auth.ProtectBranchForm{}), repo.SettingsProtectedBranchPost)
438 438
 			}, repo.MustBeNotBare)
439 439
 
440 440
 			m.Group("/hooks", func() {

+ 5
- 9
templates/repo/settings/branches.tmpl View File

@@ -39,20 +39,16 @@
39 39
 		<h4 class="ui top attached header">
40 40
 			{{.i18n.Tr "repo.settings.protected_branch"}}
41 41
 		</h4>
42
+
42 43
 		<div class="ui attached table segment">
43 44
 			<div class="ui grid padded">
44 45
 				<div class="eight wide column">
45 46
 					<div class="ui fluid dropdown selection" tabindex="0">
46
-						<select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch">
47
-							{{range .LeftBranches}}
48
-								<option value="">{{$.i18n.Tr "repo.settings.choose_branch"}}</option>
49
-								<option value="{{.}}">{{.}}</option>
50
-							{{end}}
51
-						</select><i class="dropdown icon"></i>
47
+						<i class="dropdown icon"></i>
52 48
 						<div class="default text">{{.i18n.Tr "repo.settings.choose_branch"}}</div>
53 49
 						<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
54 50
 							{{range .LeftBranches}}
55
-								<div class="item" data-value="{{.}}">{{.}}</div>
51
+								<a class="item" href="{{$.Repository.Link}}/settings/branches/{{.}}">{{.}}</a>
56 52
 							{{end}}
57 53
 						</div>
58 54
 					</div>
@@ -65,8 +61,8 @@
65 61
 						<tbody>
66 62
 							{{range .ProtectedBranches}}
67 63
 								<tr>
68
-									<td><div class="ui large label">{{.BranchName}}</div></td>
69
-									<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>
64
+									<td><div class="ui basic label blue">{{.BranchName}}</div></td>
65
+									<td class="right aligned"><a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/{{.BranchName}}">Edit</a></td>
70 66
 								</tr>
71 67
 							{{else}}
72 68
 								<tr class="center aligned"><td>{{.i18n.Tr "repo.settings.no_protected_branch"}}</td></tr>

+ 74
- 0
templates/repo/settings/protected_branch.tmpl View File

@@ -0,0 +1,74 @@
1
+{{template "base/head" .}}
2
+<div class="repository settings branches">
3
+	{{template "repo/header" .}}
4
+	{{template "repo/settings/navbar" .}}
5
+	<div class="ui container">
6
+		{{template "base/alert" .}}
7
+		<h4 class="ui top attached header">
8
+			{{.i18n.Tr "repo.settings.branch_protection" .Branch.BranchName | Str2html}}
9
+		</h4>
10
+		<div class="ui attached segment branch-protection">
11
+			<form class="ui form" action="{{.Link}}" method="post">
12
+				{{.CsrfTokenHtml}}
13
+				<div class="inline field">
14
+					<div class="ui checkbox">
15
+						<input class="enable-protection" name="protected" type="checkbox" data-target="#protection_box" {{if .Branch.IsProtected}}checked{{end}}>
16
+						<label>{{.i18n.Tr "repo.settings.protect_this_branch"}}</label>
17
+						<p class="help">{{.i18n.Tr "repo.settings.protect_this_branch_desc"}}</p>
18
+					</div>
19
+				</div>
20
+				<div id="protection_box" class="fields {{if not .Branch.IsProtected}}disabled{{end}}">
21
+					<div class="field">
22
+						<div class="ui checkbox">
23
+							<input class="enable-whitelist" name="enable_whitelist" type="checkbox" data-target="#whitelist_box" {{if .Branch.EnableWhitelist}}checked{{end}}>
24
+							<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label>
25
+							<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
26
+						</div>
27
+					</div>
28
+					<div id="whitelist_box" class="fields {{if not .Branch.EnableWhitelist}}disabled{{end}}">
29
+						<div class="whitelist field">
30
+							<label>{{.i18n.Tr "repo.settings.protect_whitelist_users"}}</label>
31
+							<div class="ui multiple search selection dropdown">
32
+								<input type="hidden" name="whitelist_users" value="{{.whitelist_users}}">
33
+								<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div>
34
+								<div class="menu">
35
+									{{range .Users}}
36
+										<div class="item" data-value="{{.ID}}">
37
+											<img class="ui mini image" src="{{.RelAvatarLink}}">
38
+											{{.Name}}
39
+										</div>
40
+									{{end}}
41
+								</div>
42
+							</div>
43
+						</div>
44
+						{{if .Owner.IsOrganization}}
45
+							<br>
46
+							<div class="whitelist field">
47
+								<label>{{.i18n.Tr "repo.settings.protect_whitelist_teams"}}</label>
48
+								<div class="ui multiple search selection dropdown">
49
+									<input type="hidden" name="whitelist_teams" value="{{.whitelist_teams}}">
50
+									<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div>
51
+									<div class="menu">
52
+										{{range .Teams}}
53
+											<div class="item" data-value="{{.ID}}">
54
+												<i class="octicon octicon-jersey"></i>
55
+												{{.Name}}
56
+											</div>
57
+										{{end}}
58
+									</div>
59
+								</div>
60
+							</div>
61
+						{{end}}
62
+					</div>
63
+				</div>
64
+
65
+				<div class="ui divider"></div>
66
+
67
+				<div class="field">
68
+					<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
69
+				</div>
70
+			</form>
71
+		</div>
72
+	</div>
73
+</div>
74
+{{template "base/footer" .}}

Loading…
Cancel
Save