Browse Source

Protected branches system (#339)

* Protected branches system

* Moved default branch to branches section (`:org/:reponame/settings/branches`).
* Initial support Protected Branch.
  - Admin does not restrict
  - Owner not to limit
  - To write permission restrictions

* reformat tmpl

* finished the UI and add/delete protected branch response

* remove unused comment

* indent all the template files and remove ru translations since we use crowdin

* fix the push bug
Denis Denisov 2 years ago
parent
commit
fd941db246

+ 4 - 0
cmd/serve.go

@@ -342,6 +342,10 @@ func runServ(c *cli.Context) error {
342 342
 	} else {
343 343
 		gitcmd = exec.Command(verb, repoPath)
344 344
 	}
345
+
346
+	os.Setenv(models.ProtectedBranchAccessMode, requestedMode.String())
347
+	os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID))
348
+
345 349
 	gitcmd.Dir = setting.RepoRootPath
346 350
 	gitcmd.Stdout = os.Stdout
347 351
 	gitcmd.Stdin = os.Stdin

+ 20 - 0
cmd/update.go

@@ -6,9 +6,12 @@ package cmd
6 6
 
7 7
 import (
8 8
 	"os"
9
+	"strconv"
10
+	"strings"
9 11
 
10 12
 	"github.com/urfave/cli"
11 13
 
14
+	"code.gitea.io/git"
12 15
 	"code.gitea.io/gitea/models"
13 16
 	"code.gitea.io/gitea/modules/log"
14 17
 	"code.gitea.io/gitea/modules/setting"
@@ -48,6 +51,23 @@ func runUpdate(c *cli.Context) error {
48 51
 		log.GitLogger.Fatal(2, "First argument 'refName' is empty, shouldn't use")
49 52
 	}
50 53
 
54
+	// protected branch check
55
+	branchName := strings.TrimPrefix(args[0], git.BranchPrefix)
56
+	repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
57
+	log.GitLogger.Trace("pushing to %d %v", repoID, branchName)
58
+	accessMode := models.ParseAccessMode(os.Getenv(models.ProtectedBranchAccessMode))
59
+	// skip admin or owner AccessMode
60
+	if accessMode == models.AccessModeWrite {
61
+		protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
62
+		if err != nil {
63
+			log.GitLogger.Fatal(2, "retrieve protected branches information failed")
64
+		}
65
+
66
+		if protectBranch != nil {
67
+			log.GitLogger.Fatal(2, "protected branches can not be pushed to")
68
+		}
69
+	}
70
+
51 71
 	task := models.UpdateTask{
52 72
 		UUID:        os.Getenv("GITEA_UUID"),
53 73
 		RefName:     args[0],

+ 5 - 0
cmd/web.go

@@ -421,6 +421,11 @@ func runWeb(ctx *cli.Context) error {
421 421
 				m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
422 422
 				m.Post("/delete", repo.DeleteCollaboration)
423 423
 			})
424
+			m.Group("/branches", func() {
425
+				m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
426
+				m.Post("/can_push", repo.ChangeProtectedBranch)
427
+				m.Post("/delete", repo.DeleteProtectedBranch)
428
+			})
424 429
 
425 430
 			m.Group("/hooks", func() {
426 431
 				m.Get("", repo.Webhooks)

+ 161 - 0
models/branches.go

@@ -0,0 +1,161 @@
1
+// Copyright 2016 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 models
6
+
7
+import (
8
+	"fmt"
9
+	"strings"
10
+	"time"
11
+)
12
+
13
+// Protected metadata
14
+const (
15
+	// Protected User ID
16
+	ProtectedBranchUserID = "GITEA_USER_ID"
17
+	// Protected Repo ID
18
+	ProtectedBranchRepoID = "GITEA_REPO_ID"
19
+	// Protected access mode
20
+	ProtectedBranchAccessMode = "GITEA_ACCESS_MODE"
21
+)
22
+
23
+// ProtectedBranch struct
24
+type ProtectedBranch struct {
25
+	ID          int64  `xorm:"pk autoincr"`
26
+	RepoID      int64  `xorm:"UNIQUE(s)"`
27
+	BranchName  string `xorm:"UNIQUE(s)"`
28
+	CanPush     bool
29
+	Created     time.Time `xorm:"-"`
30
+	CreatedUnix int64
31
+	Updated     time.Time `xorm:"-"`
32
+	UpdatedUnix int64
33
+}
34
+
35
+// BeforeInsert before protected branch insert create and update time
36
+func (protectBranch *ProtectedBranch) BeforeInsert() {
37
+	protectBranch.CreatedUnix = time.Now().Unix()
38
+	protectBranch.UpdatedUnix = protectBranch.CreatedUnix
39
+}
40
+
41
+// BeforeUpdate before protected branch update time
42
+func (protectBranch *ProtectedBranch) BeforeUpdate() {
43
+	protectBranch.UpdatedUnix = time.Now().Unix()
44
+}
45
+
46
+// GetProtectedBranchByRepoID getting protected branch by repo ID
47
+func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
48
+	protectedBranches := make([]*ProtectedBranch, 0)
49
+	return protectedBranches, x.Where("repo_id = ?", RepoID).Desc("updated_unix").Find(&protectedBranches)
50
+}
51
+
52
+// GetProtectedBranchBy getting protected branch by ID/Name
53
+func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, error) {
54
+	rel := &ProtectedBranch{RepoID: repoID, BranchName: strings.ToLower(BranchName)}
55
+	has, err := x.Get(rel)
56
+	if err != nil {
57
+		return nil, err
58
+	}
59
+	if !has {
60
+		return nil, nil
61
+	}
62
+	return rel, nil
63
+}
64
+
65
+// GetProtectedBranches get all protected btanches
66
+func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
67
+	protectedBranches := make([]*ProtectedBranch, 0)
68
+	return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
69
+}
70
+
71
+// AddProtectedBranch add protection to branch
72
+func (repo *Repository) AddProtectedBranch(branchName string, canPush bool) error {
73
+	protectedBranch := &ProtectedBranch{
74
+		RepoID:     repo.ID,
75
+		BranchName: branchName,
76
+	}
77
+
78
+	has, err := x.Get(protectedBranch)
79
+	if err != nil {
80
+		return err
81
+	} else if has {
82
+		return nil
83
+	}
84
+
85
+	sess := x.NewSession()
86
+	defer sessionRelease(sess)
87
+	if err = sess.Begin(); err != nil {
88
+		return err
89
+	}
90
+	protectedBranch.CanPush = canPush
91
+	if _, err = sess.InsertOne(protectedBranch); err != nil {
92
+		return err
93
+	}
94
+
95
+	return sess.Commit()
96
+}
97
+
98
+// ChangeProtectedBranch access mode sets new access mode for the ProtectedBranch.
99
+func (repo *Repository) ChangeProtectedBranch(id int64, canPush bool) error {
100
+	ProtectedBranch := &ProtectedBranch{
101
+		RepoID: repo.ID,
102
+		ID:     id,
103
+	}
104
+	has, err := x.Get(ProtectedBranch)
105
+	if err != nil {
106
+		return fmt.Errorf("get ProtectedBranch: %v", err)
107
+	} else if !has {
108
+		return nil
109
+	}
110
+
111
+	if ProtectedBranch.CanPush == canPush {
112
+		return nil
113
+	}
114
+	ProtectedBranch.CanPush = canPush
115
+
116
+	sess := x.NewSession()
117
+	defer sessionRelease(sess)
118
+	if err = sess.Begin(); err != nil {
119
+		return err
120
+	}
121
+
122
+	if _, err = sess.Id(ProtectedBranch.ID).AllCols().Update(ProtectedBranch); err != nil {
123
+		return fmt.Errorf("update ProtectedBranch: %v", err)
124
+	}
125
+
126
+	return sess.Commit()
127
+}
128
+
129
+// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
130
+func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
131
+	protectedBranch := &ProtectedBranch{
132
+		RepoID: repo.ID,
133
+		ID:     id,
134
+	}
135
+
136
+	sess := x.NewSession()
137
+	defer sessionRelease(sess)
138
+	if err = sess.Begin(); err != nil {
139
+		return err
140
+	}
141
+
142
+	if affected, err := sess.Delete(protectedBranch); err != nil {
143
+		return err
144
+	} else if affected != 1 {
145
+		return fmt.Errorf("delete protected branch ID(%v) failed", id)
146
+	}
147
+
148
+	return sess.Commit()
149
+}
150
+
151
+// newProtectedBranch insert one queue
152
+func newProtectedBranch(protectedBranch *ProtectedBranch) error {
153
+	_, err := x.InsertOne(protectedBranch)
154
+	return err
155
+}
156
+
157
+// UpdateProtectedBranch update queue
158
+func UpdateProtectedBranch(protectedBranch *ProtectedBranch) error {
159
+	_, err := x.Update(protectedBranch)
160
+	return err
161
+}

+ 2 - 0
models/migrations/migrations.go

@@ -82,6 +82,8 @@ var migrations = []Migration{
82 82
 	NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn),
83 83
 	// V16 -> v17
84 84
 	NewMigration("create repo unit table and add units for all repos", addUnitsToTables),
85
+	// v17 -> v18
86
+	NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
85 87
 }
86 88
 
87 89
 // Migrate database to current version

+ 29 - 0
models/migrations/v17.go

@@ -0,0 +1,29 @@
1
+// Copyright 2016 Gitea. 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
+	"github.com/go-xorm/xorm"
12
+)
13
+
14
+func setProtectedBranchUpdatedWithCreated(x *xorm.Engine) (err error) {
15
+	type ProtectedBranch struct {
16
+		ID          int64  `xorm:"pk autoincr"`
17
+		RepoID      int64  `xorm:"UNIQUE(s)"`
18
+		BranchName  string `xorm:"UNIQUE(s)"`
19
+		CanPush     bool
20
+		Created     time.Time `xorm:"-"`
21
+		CreatedUnix int64
22
+		Updated     time.Time `xorm:"-"`
23
+		UpdatedUnix int64
24
+	}
25
+	if err = x.Sync2(new(ProtectedBranch)); err != nil {
26
+		return fmt.Errorf("Sync2: %v", err)
27
+	}
28
+	return nil
29
+}

+ 6 - 0
models/repo.go

@@ -524,6 +524,12 @@ func (repo *Repository) HasAccess(u *User) bool {
524 524
 	return has
525 525
 }
526 526
 
527
+// UpdateDefaultBranch updates the default branch
528
+func (repo *Repository) UpdateDefaultBranch() error {
529
+	_, err := x.ID(repo.ID).Cols("default_branch").Update(repo)
530
+	return err
531
+}
532
+
527 533
 // IsOwnedBy returns true when user owns this repository
528 534
 func (repo *Repository) IsOwnedBy(userID int64) bool {
529 535
 	return repo.OwnerID == userID

+ 0 - 1
modules/auth/repo_form.go

@@ -88,7 +88,6 @@ type RepoSettingForm struct {
88 88
 	RepoName      string `binding:"Required;AlphaDashDot;MaxSize(100)"`
89 89
 	Description   string `binding:"MaxSize(255)"`
90 90
 	Website       string `binding:"Url;MaxSize(255)"`
91
-	Branch        string
92 91
 	Interval      int
93 92
 	MirrorAddress string
94 93
 	Private       bool

+ 2 - 2
options/locale/TRANSLATORS

@@ -49,10 +49,12 @@ Muhammad Fawwaz Orabi <mfawwaz93 AT gmail DOT com>
49 49
 Nakao Takamasa <at.mattenn AT gmail DOT com>
50 50
 Natan Albuquerque <natanalbuquerque5 AT gmail DOT com>
51 51
 Odilon Junior <odilon DOT junior93 AT gmail DOT com>
52
+Pablo Saavedra <psaavedra AT igalia DOT com>
52 53
 Richard Bukovansky <richard DOT bukovansky @ gmail DOT com>
53 54
 Robert Nuske <robert DOT nuske AT web DOT de>
54 55
 Robin Hübner <profan AT prfn DOT se>
55 56
 SeongJae Park <sj38 DOT park AT gmail DOT com>
57
+Thiago Avelino <thiago AT avelino DOT xxx>
56 58
 Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
57 59
 Tilmann Bach <tilmann AT outlook DOT com>
58 60
 Toni Villena Jiménez <tonivj5 AT gmail DOT com>
@@ -60,5 +62,3 @@ Vladimir Jigulin mogaika AT yandex DOT ru
60 62
 Vladimir Vissoultchev <wqweto AT gmail DOT com>
61 63
 YJSoft <yjsoft AT yjsoft DOT pe DOT kr>
62 64
 Łukasz Jan Niemier <lukasz AT niemier DOT pl>
63
-Pablo Saavedra <psaavedra AT igalia DOT com>
64
-Thiago Avelino <thiago AT avelino DOT xxx>

+ 12 - 0
options/locale/locale_en-US.ini

@@ -814,6 +814,18 @@ settings.add_key_success = New deploy key '%s' has been added successfully!
814 814
 settings.deploy_key_deletion = Delete Deploy Key
815 815
 settings.deploy_key_deletion_desc = Deleting this deploy key will remove all related accesses for this repository. Do you want to continue?
816 816
 settings.deploy_key_deletion_success = Deploy key has been deleted successfully!
817
+settings.branches=Branches
818
+settings.protected_branch=Branch Protection
819
+settings.protected_branch_can_push=Allow push?
820
+settings.protected_branch_can_push_yes=You can push
821
+settings.protected_branch_can_push_no=You can not push
822
+settings.add_protected_branch=Enable protection
823
+settings.delete_protected_branch=Disable protection
824
+settings.add_protected_branch_success=%s Locked successfully
825
+settings.add_protected_branch_failed= %s Locked failed
826
+settings.remove_protected_branch_success=%s Unlocked successfully
827
+settings.protected_branch_deletion=To delete a protected branch
828
+settings.protected_branch_deletion_desc=Anyone with write permissions will be able to push directly to this branch. Are you sure?
817 829
 
818 830
 diff.browse_source = Browse Source
819 831
 diff.parent = parent

+ 37 - 0
public/js/index.js

@@ -580,6 +580,42 @@ function initRepository() {
580 580
     }
581 581
 }
582 582
 
583
+function initProtectedBranch() {
584
+    $('#protectedBranch').change(function () {
585
+        var $this = $(this);
586
+        $.post($this.data('url'), {
587
+                "_csrf": csrf,
588
+                "canPush": true,
589
+                "branchName": $this.val(),
590
+            },
591
+            function (data) {
592
+                if (data.redirect) {
593
+                    window.location.href = data.redirect;
594
+                } else {
595
+                    location.reload();
596
+                }
597
+            }
598
+        );
599
+    });
600
+
601
+    $('.rm').click(function () {
602
+        var $this = $(this);
603
+        $.post($this.data('url'), {
604
+                "_csrf": csrf,
605
+                "canPush": false,
606
+                "branchName": $this.data('val'),
607
+            },
608
+            function (data) {
609
+                if (data.redirect) {
610
+                    window.location.href = data.redirect;
611
+                } else {
612
+                    location.reload();
613
+                }
614
+            }
615
+        );
616
+    });
617
+}
618
+
583 619
 function initRepositoryCollaboration() {
584 620
     console.log('initRepositoryCollaboration');
585 621
 
@@ -1402,6 +1438,7 @@ $(document).ready(function () {
1402 1438
     initEditForm();
1403 1439
     initEditor();
1404 1440
     initOrganization();
1441
+    initProtectedBranch();
1405 1442
     initWebhook();
1406 1443
     initAdmin();
1407 1444
     initCodeView();

+ 86 - 11
routers/repo/http.go

@@ -42,10 +42,20 @@ func HTTP(ctx *context.Context) {
42 42
 	} else if service == "git-upload-pack" ||
43 43
 		strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
44 44
 		isPull = true
45
+	} else if service == "git-upload-archive" ||
46
+		strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
47
+		isPull = true
45 48
 	} else {
46 49
 		isPull = (ctx.Req.Method == "GET")
47 50
 	}
48 51
 
52
+	var accessMode models.AccessMode
53
+	if isPull {
54
+		accessMode = models.AccessModeRead
55
+	} else {
56
+		accessMode = models.AccessModeWrite
57
+	}
58
+
49 59
 	isWiki := false
50 60
 	if strings.HasSuffix(reponame, ".wiki") {
51 61
 		isWiki = true
@@ -146,17 +156,12 @@ func HTTP(ctx *context.Context) {
146 156
 			}
147 157
 
148 158
 			if !isPublicPull {
149
-				var tp = models.AccessModeWrite
150
-				if isPull {
151
-					tp = models.AccessModeRead
152
-				}
153
-
154
-				has, err := models.HasAccess(authUser, repo, tp)
159
+				has, err := models.HasAccess(authUser, repo, accessMode)
155 160
 				if err != nil {
156 161
 					ctx.Handle(http.StatusInternalServerError, "HasAccess", err)
157 162
 					return
158 163
 				} else if !has {
159
-					if tp == models.AccessModeRead {
164
+					if accessMode == models.AccessModeRead {
160 165
 						has, err = models.HasAccess(authUser, repo, models.AccessModeWrite)
161 166
 						if err != nil {
162 167
 							ctx.Handle(http.StatusInternalServerError, "HasAccess2", err)
@@ -232,9 +237,20 @@ func HTTP(ctx *context.Context) {
232 237
 		}
233 238
 	}
234 239
 
240
+	params := make(map[string]string)
241
+
242
+	if askAuth {
243
+		params[models.ProtectedBranchUserID] = fmt.Sprintf("%d", authUser.ID)
244
+		if err == nil {
245
+			params[models.ProtectedBranchAccessMode] = accessMode.String()
246
+		}
247
+		params[models.ProtectedBranchRepoID] = fmt.Sprintf("%d", repo.ID)
248
+	}
249
+
235 250
 	HTTPBackend(ctx, &serviceConfig{
236 251
 		UploadPack:  true,
237 252
 		ReceivePack: true,
253
+		Params:      params,
238 254
 		OnSucceed:   callback,
239 255
 	})(ctx.Resp, ctx.Req.Request)
240 256
 
@@ -244,6 +260,7 @@ func HTTP(ctx *context.Context) {
244 260
 type serviceConfig struct {
245 261
 	UploadPack  bool
246 262
 	ReceivePack bool
263
+	Params      map[string]string
247 264
 	OnSucceed   func(rpc string, input []byte)
248 265
 }
249 266
 
@@ -261,6 +278,42 @@ func (h *serviceHandler) setHeaderNoCache() {
261 278
 	h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
262 279
 }
263 280
 
281
+func (h *serviceHandler) getBranch(input []byte) string {
282
+	var lastLine int64
283
+	var branchName string
284
+	for {
285
+		head := input[lastLine : lastLine+2]
286
+		if head[0] == '0' && head[1] == '0' {
287
+			size, err := strconv.ParseInt(string(input[lastLine+2:lastLine+4]), 16, 32)
288
+			if err != nil {
289
+				log.Error(4, "%v", err)
290
+				return branchName
291
+			}
292
+
293
+			if size == 0 {
294
+				//fmt.Println(string(input[lastLine:]))
295
+				break
296
+			}
297
+
298
+			line := input[lastLine : lastLine+size]
299
+			idx := bytes.IndexRune(line, '\000')
300
+			if idx > -1 {
301
+				line = line[:idx]
302
+			}
303
+
304
+			fields := strings.Fields(string(line))
305
+			if len(fields) >= 3 {
306
+				refFullName := fields[2]
307
+				branchName = strings.TrimPrefix(refFullName, git.BranchPrefix)
308
+			}
309
+			lastLine = lastLine + size
310
+		} else {
311
+			break
312
+		}
313
+	}
314
+	return branchName
315
+}
316
+
264 317
 func (h *serviceHandler) setHeaderCacheForever() {
265 318
 	now := time.Now().Unix()
266 319
 	expires := now + 31536000
@@ -358,13 +411,15 @@ func serviceRPC(h serviceHandler, service string) {
358 411
 		h.w.WriteHeader(http.StatusUnauthorized)
359 412
 		return
360 413
 	}
414
+
361 415
 	h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
362 416
 
363 417
 	var (
364
-		reqBody = h.r.Body
365
-		input   []byte
366
-		br      io.Reader
367
-		err     error
418
+		reqBody    = h.r.Body
419
+		input      []byte
420
+		br         io.Reader
421
+		err        error
422
+		branchName string
368 423
 	)
369 424
 
370 425
 	// Handle GZIP.
@@ -385,11 +440,31 @@ func serviceRPC(h serviceHandler, service string) {
385 440
 			return
386 441
 		}
387 442
 
443
+		branchName = h.getBranch(input)
388 444
 		br = bytes.NewReader(input)
389 445
 	} else {
390 446
 		br = reqBody
391 447
 	}
392 448
 
449
+	// check protected branch
450
+	repoID, _ := strconv.ParseInt(h.cfg.Params[models.ProtectedBranchRepoID], 10, 64)
451
+	accessMode := models.ParseAccessMode(h.cfg.Params[models.ProtectedBranchAccessMode])
452
+	// skip admin or owner AccessMode
453
+	if accessMode == models.AccessModeWrite {
454
+		protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
455
+		if err != nil {
456
+			log.GitLogger.Error(2, "fail to get protected branch information: %v", err)
457
+			h.w.WriteHeader(http.StatusInternalServerError)
458
+			return
459
+		}
460
+
461
+		if protectBranch != nil {
462
+			log.GitLogger.Error(2, "protected branches can not be pushed to")
463
+			h.w.WriteHeader(http.StatusForbidden)
464
+			return
465
+		}
466
+	}
467
+
393 468
 	cmd := exec.Command("git", service, "--stateless-rpc", h.dir)
394 469
 	cmd.Dir = h.dir
395 470
 	cmd.Stdout = h.w

+ 137 - 11
routers/repo/setting.go

@@ -21,6 +21,7 @@ import (
21 21
 const (
22 22
 	tplSettingsOptions base.TplName = "repo/settings/options"
23 23
 	tplCollaboration   base.TplName = "repo/settings/collaboration"
24
+	tplBranches        base.TplName = "repo/settings/branches"
24 25
 	tplGithooks        base.TplName = "repo/settings/githooks"
25 26
 	tplGithookEdit     base.TplName = "repo/settings/githook_edit"
26 27
 	tplDeployKeys      base.TplName = "repo/settings/deploy_keys"
@@ -78,17 +79,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
78 79
 		// In case it's just a case change.
79 80
 		repo.Name = newRepoName
80 81
 		repo.LowerName = strings.ToLower(newRepoName)
81
-
82
-		if ctx.Repo.GitRepo.IsBranchExist(form.Branch) &&
83
-			repo.DefaultBranch != form.Branch {
84
-			repo.DefaultBranch = form.Branch
85
-			if err := ctx.Repo.GitRepo.SetDefaultBranch(form.Branch); err != nil {
86
-				if !git.IsErrUnsupportedVersion(err) {
87
-					ctx.Handle(500, "SetDefaultBranch", err)
88
-					return
89
-				}
90
-			}
91
-		}
92 82
 		repo.Description = form.Description
93 83
 		repo.Website = form.Website
94 84
 
@@ -429,6 +419,142 @@ func DeleteCollaboration(ctx *context.Context) {
429 419
 	})
430 420
 }
431 421
 
422
+// ProtectedBranch render the page to protect the repository
423
+func ProtectedBranch(ctx *context.Context) {
424
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
425
+	ctx.Data["PageIsSettingsBranches"] = true
426
+
427
+	protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
428
+	if err != nil {
429
+		ctx.Handle(500, "GetProtectedBranches", err)
430
+		return
431
+	}
432
+	ctx.Data["ProtectedBranches"] = protectedBranches
433
+
434
+	branches := ctx.Data["Branches"].([]string)
435
+	leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
436
+	for _, b := range branches {
437
+		var protected bool
438
+		for _, pb := range protectedBranches {
439
+			if b == pb.BranchName {
440
+				protected = true
441
+				break
442
+			}
443
+		}
444
+		if !protected {
445
+			leftBranches = append(leftBranches, b)
446
+		}
447
+	}
448
+
449
+	ctx.Data["LeftBranches"] = leftBranches
450
+
451
+	ctx.HTML(200, tplBranches)
452
+}
453
+
454
+// ProtectedBranchPost response for protect for a branch of a repository
455
+func ProtectedBranchPost(ctx *context.Context) {
456
+	ctx.Data["Title"] = ctx.Tr("repo.settings")
457
+	ctx.Data["PageIsSettingsBranches"] = true
458
+
459
+	repo := ctx.Repo.Repository
460
+
461
+	switch ctx.Query("action") {
462
+	case "default_branch":
463
+		if ctx.HasError() {
464
+			ctx.HTML(200, tplBranches)
465
+			return
466
+		}
467
+
468
+		branch := strings.ToLower(ctx.Query("branch"))
469
+		if ctx.Repo.GitRepo.IsBranchExist(branch) &&
470
+			repo.DefaultBranch != branch {
471
+			repo.DefaultBranch = branch
472
+			if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
473
+				if !git.IsErrUnsupportedVersion(err) {
474
+					ctx.Handle(500, "SetDefaultBranch", err)
475
+					return
476
+				}
477
+			}
478
+			if err := repo.UpdateDefaultBranch(); err != nil {
479
+				ctx.Handle(500, "SetDefaultBranch", err)
480
+				return
481
+			}
482
+		}
483
+
484
+		log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
485
+
486
+		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
487
+		ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
488
+	case "protected_branch":
489
+		if ctx.HasError() {
490
+			ctx.JSON(200, map[string]string{
491
+				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
492
+			})
493
+			return
494
+		}
495
+
496
+		branchName := strings.ToLower(ctx.Query("branchName"))
497
+		if len(branchName) == 0 || !ctx.Repo.GitRepo.IsBranchExist(branchName) {
498
+			ctx.JSON(200, map[string]string{
499
+				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
500
+			})
501
+			return
502
+		}
503
+
504
+		canPush := ctx.QueryBool("canPush")
505
+
506
+		if canPush {
507
+			if err := ctx.Repo.Repository.AddProtectedBranch(branchName, canPush); err != nil {
508
+				ctx.Flash.Error(ctx.Tr("repo.settings.add_protected_branch_failed", branchName))
509
+				ctx.JSON(200, map[string]string{
510
+					"status": "ok",
511
+				})
512
+				return
513
+			}
514
+
515
+			ctx.Flash.Success(ctx.Tr("repo.settings.add_protected_branch_success", branchName))
516
+			ctx.JSON(200, map[string]string{
517
+				"redirect": setting.AppSubURL + ctx.Req.URL.Path,
518
+			})
519
+		} else {
520
+			if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
521
+				ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
522
+			} else {
523
+				ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branchName))
524
+			}
525
+
526
+			ctx.JSON(200, map[string]interface{}{
527
+				"status": "ok",
528
+			})
529
+		}
530
+	default:
531
+		ctx.Handle(404, "", nil)
532
+	}
533
+}
534
+
535
+// ChangeProtectedBranch response for changing access of a protect branch
536
+func ChangeProtectedBranch(ctx *context.Context) {
537
+	if err := ctx.Repo.Repository.ChangeProtectedBranch(
538
+		ctx.QueryInt64("id"),
539
+		ctx.QueryBool("canPush")); err != nil {
540
+		log.Error(4, "ChangeProtectedBranch: %v", err)
541
+	}
542
+}
543
+
544
+// DeleteProtectedBranch delete a protection for a branch of a repository
545
+func DeleteProtectedBranch(ctx *context.Context) {
546
+	if err := ctx.Repo.Repository.DeleteProtectedBranch(ctx.QueryInt64("id")); err != nil {
547
+		ctx.Flash.Error("DeleteProtectedBranch: " + err.Error())
548
+	} else {
549
+		ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success"))
550
+	}
551
+
552
+	ctx.JSON(200, map[string]interface{}{
553
+		"redirect": ctx.Repo.RepoLink + "/settings/branches",
554
+	})
555
+}
556
+
557
+// parseOwnerAndRepo get repos by owner
432 558
 func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
433 559
 	owner, err := models.GetUserByName(ctx.Params(":username"))
434 560
 	if err != nil {

+ 91 - 0
templates/repo/settings/branches.tmpl

@@ -0,0 +1,91 @@
1
+{{template "base/head" .}}
2
+<div class="repository settings edit">
3
+	{{template "repo/header" .}}
4
+	<div class="ui container">
5
+		<div class="ui grid">
6
+			{{template "repo/settings/navbar" .}}
7
+			<div class="twelve wide column content">
8
+				{{template "base/alert" .}}
9
+				<h4 class="ui top attached header">
10
+					{{.i18n.Tr "repo.default_branch"}}
11
+				</h4>
12
+				<div class="ui attached table segment">
13
+					<form class="ui hook list form" action="{{.Link}}" method="post">
14
+						{{.CsrfTokenHtml}}
15
+						<input type="hidden" name="action" value="default_branch">
16
+						<div class="item">
17
+							The default branch is considered the "base" branch in your repository,
18
+							against which all pull requests and code commits are automatically made,
19
+							unless you specify a different branch.
20
+						</div>
21
+						{{if not .Repository.IsBare}}
22
+						<div class="ui grid padded">
23
+							<div class="eight wide column">
24
+								<div class="ui fluid dropdown selection visible" tabindex="0">
25
+									<select name="branch">
26
+										<option value="{{.Repository.DefaultBranch}}">{{.Repository.DefaultBranch}}</option>
27
+										{{range .Branches}}
28
+											<option value="{{.}}">{{.}}</option>
29
+										{{end}}
30
+          							</select><i class="dropdown icon"></i>
31
+									<div class="default text">{{.Repository.DefaultBranch}}</div>
32
+									<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
33
+										{{range .Branches}}
34
+											<div class="item" data-value="{{.}}">{{.}}</div>
35
+										{{end}}
36
+									</div>
37
+								</div>
38
+							</div>
39
+						</div>
40
+						{{end}}
41
+						<div class="item field">
42
+							<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
43
+						</div>
44
+					</form>
45
+				</div>
46
+
47
+				<h4 class="ui top attached header">
48
+					{{.i18n.Tr "repo.settings.protected_branch"}}
49
+				</h4>
50
+				<div class="ui attached table segment">
51
+					<div class="ui grid padded">
52
+						<div class="eight wide column">
53
+							<div class="ui fluid dropdown selection visible" tabindex="0">
54
+								<select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch">
55
+									{{range .LeftBranches}}
56
+										<option value="">Choose a branch...</option>
57
+										<option value="{{.}}">{{.}}</option>
58
+									{{end}}
59
+								</select><i class="dropdown icon"></i>
60
+								<div class="default text">Choose a branch...</div>
61
+								<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
62
+									{{range .LeftBranches}}
63
+										<div class="item" data-value="{{.}}">{{.}}</div>
64
+									{{end}}
65
+								</div>
66
+							</div>
67
+						</div>
68
+					</div>
69
+
70
+					<div class="ui grid padded">
71
+						<div class="sixteen wide column">
72
+							<table class="ui single line table padded">
73
+								<tbody>
74
+									{{range .ProtectedBranches}}
75
+										<tr>
76
+											<td><div class="ui large label">{{.BranchName}}</div></td>
77
+											<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>
78
+										</tr>
79
+									{{else}}
80
+										<tr class="center aligned"><td>There is no protected branch</td></tr>
81
+									{{end}}
82
+								</tbody>
83
+							</table>
84
+						</div>
85
+					</div>
86
+				</div>
87
+			</div>
88
+		</div>
89
+	</div>
90
+</div>
91
+{{template "base/footer" .}}

+ 1 - 0
templates/repo/settings/nav.tmpl

@@ -4,6 +4,7 @@
4 4
 		<ul class="menu menu-vertical switching-list grid-1-5 left">
5 5
 			<li {{if .PageIsSettingsOptions}}class="current"{{end}}><a href="{{.RepoLink}}/settings">{{.i18n.Tr "repo.settings.options"}}</a></li>
6 6
 			<li {{if .PageIsSettingsCollaboration}}class="current"{{end}}><a href="{{.RepoLink}}/settings/collaboration">{{.i18n.Tr "repo.settings.collaboration"}}</a></li>
7
+			<li {{if .PageIsSettingsBranches}}class="current"{{end}}><a href="{{.RepoLink}}/settings/branches">{{.i18n.Tr "repo.settings.branches"}}</a></li>
7 8
 			<li {{if .PageIsSettingsHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks">{{.i18n.Tr "repo.settings.hooks"}}</a></li>
8 9
 			{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}}
9 10
 				<li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.i18n.Tr "repo.settings.githooks"}}</a></li>

+ 5 - 0
templates/repo/settings/navbar.tmpl

@@ -7,6 +7,11 @@
7 7
 		<a class="{{if .PageIsSettingsCollaboration}}active{{end}} item" href="{{.RepoLink}}/settings/collaboration">
8 8
 			{{.i18n.Tr "repo.settings.collaboration"}}
9 9
 		</a>
10
+		{{if not .Repository.IsBare}}
11
+			<a class="{{if .PageIsSettingsBranches}}active{{end}} item" href="{{.RepoLink}}/settings/branches">
12
+				{{.i18n.Tr "repo.settings.branches"}}
13
+			</a>
14
+		{{end}}
10 15
 		<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks">
11 16
 			{{.i18n.Tr "repo.settings.hooks"}}
12 17
 		</a>

+ 8 - 24
templates/repo/settings/options.tmpl

@@ -17,30 +17,6 @@
17 17
 							<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
18 18
 							<input id="repo_name" name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" autofocus required>
19 19
 						</div>
20
-						<div class="field {{if .Err_Description}}error{{end}}">
21
-							<label for="description">{{$.i18n.Tr "repo.repo_desc"}}</label>
22
-							<textarea id="description" name="description" rows="2">{{.Repository.Description}}</textarea>
23
-						</div>
24
-						<div class="field {{if .Err_Website}}error{{end}}">
25
-							<label for="website">{{.i18n.Tr "repo.settings.site"}}</label>
26
-							<input id="website" name="website" type="url" value="{{.Repository.Website}}">
27
-						</div>
28
-
29
-						{{if not .Repository.IsBare}}
30
-							<div class="required inline field">
31
-								<label>{{.i18n.Tr "repo.default_branch"}}</label>
32
-								<div class="ui selection dropdown">
33
-									<input type="hidden" id="branch" name="branch" value="{{.Repository.DefaultBranch}}">
34
-									<div class="text">{{.Repository.DefaultBranch}}</div>
35
-									<i class="dropdown icon"></i>
36
-									<div class="menu">
37
-										{{range .Branches}}
38
-											<div class="item" data-value="{{.}}">{{.}}</div>
39
-										{{end}}
40
-									</div>
41
-								</div>
42
-							</div>
43
-						{{end}}
44 20
 						{{if not .Repository.IsFork}}
45 21
 							<div class="inline field">
46 22
 								<label>{{.i18n.Tr "repo.visibility"}}</label>
@@ -50,6 +26,14 @@
50 26
 								</div>
51 27
 							</div>
52 28
 						{{end}}
29
+						<div class="field {{if .Err_Description}}error{{end}}">
30
+							<label for="description">{{$.i18n.Tr "repo.repo_desc"}}</label>
31
+							<textarea id="description" name="description" rows="2">{{.Repository.Description}}</textarea>
32
+						</div>
33
+						<div class="field {{if .Err_Website}}error{{end}}">
34
+							<label for="website">{{.i18n.Tr "repo.settings.site"}}</label>
35
+							<input id="website" name="website" type="url" value="{{.Repository.Website}}">
36
+						</div>
53 37
 
54 38
 						<div class="field">
55 39
 							<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>