summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/serve.go4
-rw-r--r--cmd/update.go20
-rw-r--r--cmd/web.go5
-rw-r--r--models/branches.go161
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v17.go29
-rw-r--r--models/repo.go6
-rw-r--r--modules/auth/repo_form.go1
-rw-r--r--options/locale/TRANSLATORS4
-rw-r--r--options/locale/locale_en-US.ini12
-rw-r--r--public/js/index.js37
-rw-r--r--routers/repo/http.go97
-rw-r--r--routers/repo/setting.go148
-rw-r--r--templates/repo/settings/branches.tmpl91
-rw-r--r--templates/repo/settings/nav.tmpl1
-rw-r--r--templates/repo/settings/navbar.tmpl5
-rw-r--r--templates/repo/settings/options.tmpl32
17 files changed, 606 insertions, 49 deletions
diff --git a/cmd/serve.go b/cmd/serve.go
index 7141d85c92..f4a3c3d2c6 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -342,6 +342,10 @@ func runServ(c *cli.Context) error {
} else {
gitcmd = exec.Command(verb, repoPath)
}
+
+ os.Setenv(models.ProtectedBranchAccessMode, requestedMode.String())
+ os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID))
+
gitcmd.Dir = setting.RepoRootPath
gitcmd.Stdout = os.Stdout
gitcmd.Stdin = os.Stdin
diff --git a/cmd/update.go b/cmd/update.go
index 4bbab9a3af..58e60493d0 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -6,9 +6,12 @@ package cmd
import (
"os"
+ "strconv"
+ "strings"
"github.com/urfave/cli"
+ "code.gitea.io/git"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -48,6 +51,23 @@ func runUpdate(c *cli.Context) error {
log.GitLogger.Fatal(2, "First argument 'refName' is empty, shouldn't use")
}
+ // protected branch check
+ branchName := strings.TrimPrefix(args[0], git.BranchPrefix)
+ repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
+ log.GitLogger.Trace("pushing to %d %v", repoID, branchName)
+ accessMode := models.ParseAccessMode(os.Getenv(models.ProtectedBranchAccessMode))
+ // skip admin or owner AccessMode
+ if accessMode == models.AccessModeWrite {
+ protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
+ if err != nil {
+ log.GitLogger.Fatal(2, "retrieve protected branches information failed")
+ }
+
+ if protectBranch != nil {
+ log.GitLogger.Fatal(2, "protected branches can not be pushed to")
+ }
+ }
+
task := models.UpdateTask{
UUID: os.Getenv("GITEA_UUID"),
RefName: args[0],
diff --git a/cmd/web.go b/cmd/web.go
index 03a87ca0d6..f9b4bdd270 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -421,6 +421,11 @@ func runWeb(ctx *cli.Context) error {
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
m.Post("/delete", repo.DeleteCollaboration)
})
+ m.Group("/branches", func() {
+ m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
+ m.Post("/can_push", repo.ChangeProtectedBranch)
+ m.Post("/delete", repo.DeleteProtectedBranch)
+ })
m.Group("/hooks", func() {
m.Get("", repo.Webhooks)
diff --git a/models/branches.go b/models/branches.go
new file mode 100644
index 0000000000..a02ff1b8bb
--- /dev/null
+++ b/models/branches.go
@@ -0,0 +1,161 @@
+// Copyright 2016 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 models
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+// Protected metadata
+const (
+ // Protected User ID
+ ProtectedBranchUserID = "GITEA_USER_ID"
+ // Protected Repo ID
+ ProtectedBranchRepoID = "GITEA_REPO_ID"
+ // Protected access mode
+ ProtectedBranchAccessMode = "GITEA_ACCESS_MODE"
+)
+
+// 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
+ Updated time.Time `xorm:"-"`
+ UpdatedUnix int64
+}
+
+// BeforeInsert before protected branch insert create and update time
+func (protectBranch *ProtectedBranch) BeforeInsert() {
+ protectBranch.CreatedUnix = time.Now().Unix()
+ protectBranch.UpdatedUnix = protectBranch.CreatedUnix
+}
+
+// BeforeUpdate before protected branch update time
+func (protectBranch *ProtectedBranch) BeforeUpdate() {
+ protectBranch.UpdatedUnix = time.Now().Unix()
+}
+
+// GetProtectedBranchByRepoID getting protected branch by repo ID
+func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) {
+ protectedBranches := make([]*ProtectedBranch, 0)
+ return protectedBranches, x.Where("repo_id = ?", RepoID).Desc("updated_unix").Find(&protectedBranches)
+}
+
+// GetProtectedBranchBy getting protected branch by ID/Name
+func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, error) {
+ rel := &ProtectedBranch{RepoID: repoID, BranchName: strings.ToLower(BranchName)}
+ has, err := x.Get(rel)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, nil
+ }
+ return rel, nil
+}
+
+// GetProtectedBranches get all protected btanches
+func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
+ protectedBranches := make([]*ProtectedBranch, 0)
+ return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
+}
+
+// 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 sessionRelease(sess)
+ 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 sessionRelease(sess)
+ 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{
+ RepoID: repo.ID,
+ ID: id,
+ }
+
+ sess := x.NewSession()
+ defer sessionRelease(sess)
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if affected, err := sess.Delete(protectedBranch); err != nil {
+ return err
+ } else if affected != 1 {
+ return fmt.Errorf("delete protected branch ID(%v) failed", id)
+ }
+
+ return sess.Commit()
+}
+
+// newProtectedBranch insert one queue
+func newProtectedBranch(protectedBranch *ProtectedBranch) error {
+ _, err := x.InsertOne(protectedBranch)
+ return err
+}
+
+// UpdateProtectedBranch update queue
+func UpdateProtectedBranch(protectedBranch *ProtectedBranch) error {
+ _, err := x.Update(protectedBranch)
+ return err
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 9832cdca92..e54d502b7d 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -82,6 +82,8 @@ var migrations = []Migration{
NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn),
// V16 -> v17
NewMigration("create repo unit table and add units for all repos", addUnitsToTables),
+ // v17 -> v18
+ NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
}
// Migrate database to current version
diff --git a/models/migrations/v17.go b/models/migrations/v17.go
new file mode 100644
index 0000000000..2986badc97
--- /dev/null
+++ b/models/migrations/v17.go
@@ -0,0 +1,29 @@
+// Copyright 2016 Gitea. 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"
+
+ "github.com/go-xorm/xorm"
+)
+
+func setProtectedBranchUpdatedWithCreated(x *xorm.Engine) (err 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
+ }
+ if err = x.Sync2(new(ProtectedBranch)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ return nil
+}
diff --git a/models/repo.go b/models/repo.go
index 490c8a3274..1d4e3dc311 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -524,6 +524,12 @@ func (repo *Repository) HasAccess(u *User) bool {
return has
}
+// UpdateDefaultBranch updates the default branch
+func (repo *Repository) UpdateDefaultBranch() error {
+ _, err := x.ID(repo.ID).Cols("default_branch").Update(repo)
+ return err
+}
+
// IsOwnedBy returns true when user owns this repository
func (repo *Repository) IsOwnedBy(userID int64) bool {
return repo.OwnerID == userID
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 82018ea0ff..2b118ce48d 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -88,7 +88,6 @@ type RepoSettingForm struct {
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Description string `binding:"MaxSize(255)"`
Website string `binding:"Url;MaxSize(255)"`
- Branch string
Interval int
MirrorAddress string
Private bool
diff --git a/options/locale/TRANSLATORS b/options/locale/TRANSLATORS
index 651c830997..b8354d0a55 100644
--- a/options/locale/TRANSLATORS
+++ b/options/locale/TRANSLATORS
@@ -49,10 +49,12 @@ Muhammad Fawwaz Orabi <mfawwaz93 AT gmail DOT com>
Nakao Takamasa <at.mattenn AT gmail DOT com>
Natan Albuquerque <natanalbuquerque5 AT gmail DOT com>
Odilon Junior <odilon DOT junior93 AT gmail DOT com>
+Pablo Saavedra <psaavedra AT igalia DOT com>
Richard Bukovansky <richard DOT bukovansky @ gmail DOT com>
Robert Nuske <robert DOT nuske AT web DOT de>
Robin Hübner <profan AT prfn DOT se>
SeongJae Park <sj38 DOT park AT gmail DOT com>
+Thiago Avelino <thiago AT avelino DOT xxx>
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
Tilmann Bach <tilmann AT outlook DOT com>
Toni Villena Jiménez <tonivj5 AT gmail DOT com>
@@ -60,5 +62,3 @@ Vladimir Jigulin mogaika AT yandex DOT ru
Vladimir Vissoultchev <wqweto AT gmail DOT com>
YJSoft <yjsoft AT yjsoft DOT pe DOT kr>
Łukasz Jan Niemier <lukasz AT niemier DOT pl>
-Pablo Saavedra <psaavedra AT igalia DOT com>
-Thiago Avelino <thiago AT avelino DOT xxx>
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9ae5f369b2..d33e5ea984 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -814,6 +814,18 @@ settings.add_key_success = New deploy key '%s' has been added successfully!
settings.deploy_key_deletion = Delete Deploy Key
settings.deploy_key_deletion_desc = Deleting this deploy key will remove all related accesses for this repository. Do you want to continue?
settings.deploy_key_deletion_success = Deploy key has been deleted successfully!
+settings.branches=Branches
+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.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.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?
diff.browse_source = Browse Source
diff.parent = parent
diff --git a/public/js/index.js b/public/js/index.js
index f443e52436..fb35eadf8a 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -580,6 +580,42 @@ function initRepository() {
}
}
+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();
+ }
+ }
+ );
+ });
+}
+
function initRepositoryCollaboration() {
console.log('initRepositoryCollaboration');
@@ -1402,6 +1438,7 @@ $(document).ready(function () {
initEditForm();
initEditor();
initOrganization();
+ initProtectedBranch();
initWebhook();
initAdmin();
initCodeView();
diff --git a/routers/repo/http.go b/routers/repo/http.go
index 695e758cdb..780babd40d 100644
--- a/routers/repo/http.go
+++ b/routers/repo/http.go
@@ -42,10 +42,20 @@ func HTTP(ctx *context.Context) {
} else if service == "git-upload-pack" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
isPull = true
+ } else if service == "git-upload-archive" ||
+ strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
+ isPull = true
} else {
isPull = (ctx.Req.Method == "GET")
}
+ var accessMode models.AccessMode
+ if isPull {
+ accessMode = models.AccessModeRead
+ } else {
+ accessMode = models.AccessModeWrite
+ }
+
isWiki := false
if strings.HasSuffix(reponame, ".wiki") {
isWiki = true
@@ -146,17 +156,12 @@ func HTTP(ctx *context.Context) {
}
if !isPublicPull {
- var tp = models.AccessModeWrite
- if isPull {
- tp = models.AccessModeRead
- }
-
- has, err := models.HasAccess(authUser, repo, tp)
+ has, err := models.HasAccess(authUser, repo, accessMode)
if err != nil {
ctx.Handle(http.StatusInternalServerError, "HasAccess", err)
return
} else if !has {
- if tp == models.AccessModeRead {
+ if accessMode == models.AccessModeRead {
has, err = models.HasAccess(authUser, repo, models.AccessModeWrite)
if err != nil {
ctx.Handle(http.StatusInternalServerError, "HasAccess2", err)
@@ -232,9 +237,20 @@ func HTTP(ctx *context.Context) {
}
}
+ params := make(map[string]string)
+
+ if askAuth {
+ params[models.ProtectedBranchUserID] = fmt.Sprintf("%d", authUser.ID)
+ if err == nil {
+ params[models.ProtectedBranchAccessMode] = accessMode.String()
+ }
+ params[models.ProtectedBranchRepoID] = fmt.Sprintf("%d", repo.ID)
+ }
+
HTTPBackend(ctx, &serviceConfig{
UploadPack: true,
ReceivePack: true,
+ Params: params,
OnSucceed: callback,
})(ctx.Resp, ctx.Req.Request)
@@ -244,6 +260,7 @@ func HTTP(ctx *context.Context) {
type serviceConfig struct {
UploadPack bool
ReceivePack bool
+ Params map[string]string
OnSucceed func(rpc string, input []byte)
}
@@ -261,6 +278,42 @@ func (h *serviceHandler) setHeaderNoCache() {
h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
}
+func (h *serviceHandler) getBranch(input []byte) string {
+ var lastLine int64
+ var branchName string
+ for {
+ head := input[lastLine : lastLine+2]
+ if head[0] == '0' && head[1] == '0' {
+ size, err := strconv.ParseInt(string(input[lastLine+2:lastLine+4]), 16, 32)
+ if err != nil {
+ log.Error(4, "%v", err)
+ return branchName
+ }
+
+ if size == 0 {
+ //fmt.Println(string(input[lastLine:]))
+ break
+ }
+
+ line := input[lastLine : lastLine+size]
+ idx := bytes.IndexRune(line, '\000')
+ if idx > -1 {
+ line = line[:idx]
+ }
+
+ fields := strings.Fields(string(line))
+ if len(fields) >= 3 {
+ refFullName := fields[2]
+ branchName = strings.TrimPrefix(refFullName, git.BranchPrefix)
+ }
+ lastLine = lastLine + size
+ } else {
+ break
+ }
+ }
+ return branchName
+}
+
func (h *serviceHandler) setHeaderCacheForever() {
now := time.Now().Unix()
expires := now + 31536000
@@ -358,13 +411,15 @@ func serviceRPC(h serviceHandler, service string) {
h.w.WriteHeader(http.StatusUnauthorized)
return
}
+
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
var (
- reqBody = h.r.Body
- input []byte
- br io.Reader
- err error
+ reqBody = h.r.Body
+ input []byte
+ br io.Reader
+ err error
+ branchName string
)
// Handle GZIP.
@@ -385,11 +440,31 @@ func serviceRPC(h serviceHandler, service string) {
return
}
+ branchName = h.getBranch(input)
br = bytes.NewReader(input)
} else {
br = reqBody
}
+ // check protected branch
+ repoID, _ := strconv.ParseInt(h.cfg.Params[models.ProtectedBranchRepoID], 10, 64)
+ accessMode := models.ParseAccessMode(h.cfg.Params[models.ProtectedBranchAccessMode])
+ // skip admin or owner AccessMode
+ if accessMode == models.AccessModeWrite {
+ protectBranch, err := models.GetProtectedBranchBy(repoID, branchName)
+ if err != nil {
+ log.GitLogger.Error(2, "fail to get protected branch information: %v", err)
+ h.w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if protectBranch != nil {
+ log.GitLogger.Error(2, "protected branches can not be pushed to")
+ h.w.WriteHeader(http.StatusForbidden)
+ return
+ }
+ }
+
cmd := exec.Command("git", service, "--stateless-rpc", h.dir)
cmd.Dir = h.dir
cmd.Stdout = h.w
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 17a5b4aa02..91068d242a 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -21,6 +21,7 @@ import (
const (
tplSettingsOptions base.TplName = "repo/settings/options"
tplCollaboration base.TplName = "repo/settings/collaboration"
+ tplBranches base.TplName = "repo/settings/branches"
tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys"
@@ -78,17 +79,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
// In case it's just a case change.
repo.Name = newRepoName
repo.LowerName = strings.ToLower(newRepoName)
-
- if ctx.Repo.GitRepo.IsBranchExist(form.Branch) &&
- repo.DefaultBranch != form.Branch {
- repo.DefaultBranch = form.Branch
- if err := ctx.Repo.GitRepo.SetDefaultBranch(form.Branch); err != nil {
- if !git.IsErrUnsupportedVersion(err) {
- ctx.Handle(500, "SetDefaultBranch", err)
- return
- }
- }
- }
repo.Description = form.Description
repo.Website = form.Website
@@ -429,6 +419,142 @@ 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 := strings.ToLower(ctx.Query("branch"))
+ if ctx.Repo.GitRepo.IsBranchExist(branch) &&
+ 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"))
if err != nil {
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
new file mode 100644
index 0000000000..79dd05392e
--- /dev/null
+++ b/templates/repo/settings/branches.tmpl
@@ -0,0 +1,91 @@
+{{template "base/head" .}}
+<div class="repository settings edit">
+ {{template "repo/header" .}}
+ <div class="ui container">
+ <div class="ui grid">
+ {{template "repo/settings/navbar" .}}
+ <div class="twelve wide column content">
+ {{template "base/alert" .}}
+ <h4 class="ui top attached header">
+ {{.i18n.Tr "repo.default_branch"}}
+ </h4>
+ <div class="ui attached table segment">
+ <form class="ui hook list form" action="{{.Link}}" method="post">
+ {{.CsrfTokenHtml}}
+ <input type="hidden" name="action" value="default_branch">
+ <div class="item">
+ 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.
+ </div>
+ {{if not .Repository.IsBare}}
+ <div class="ui grid padded">
+ <div class="eight wide column">
+ <div class="ui fluid dropdown selection visible" tabindex="0">
+ <select name="branch">
+ <option value="{{.Repository.DefaultBranch}}">{{.Repository.DefaultBranch}}</option>
+ {{range .Branches}}
+ <option value="{{.}}">{{.}}</option>
+ {{end}}
+ </select><i class="dropdown icon"></i>
+ <div class="default text">{{.Repository.DefaultBranch}}</div>
+ <div class="menu transition hidden" tabindex="-1" style="display: block !important;">
+ {{range .Branches}}
+ <div class="item" data-value="{{.}}">{{.}}</div>
+ {{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ <div class="item field">
+ <button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
+ </div>
+ </form>
+ </div>
+
+ <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 visible" tabindex="0">
+ <select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch">
+ {{range .LeftBranches}}
+ <option value="">Choose a branch...</option>
+ <option value="{{.}}">{{.}}</option>
+ {{end}}
+ </select><i class="dropdown icon"></i>
+ <div class="default text">Choose a branch...</div>
+ <div class="menu transition hidden" tabindex="-1" style="display: block !important;">
+ {{range .LeftBranches}}
+ <div class="item" data-value="{{.}}">{{.}}</div>
+ {{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="ui grid padded">
+ <div class="sixteen wide column">
+ <table class="ui single line table padded">
+ <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>
+ </tr>
+ {{else}}
+ <tr class="center aligned"><td>There is no protected branch</td></tr>
+ {{end}}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+{{template "base/footer" .}} \ No newline at end of file
diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl
index 97df429f2c..5cc77e1dc9 100644
--- a/templates/repo/settings/nav.tmpl
+++ b/templates/repo/settings/nav.tmpl
@@ -4,6 +4,7 @@
<ul class="menu menu-vertical switching-list grid-1-5 left">
<li {{if .PageIsSettingsOptions}}class="current"{{end}}><a href="{{.RepoLink}}/settings">{{.i18n.Tr "repo.settings.options"}}</a></li>
<li {{if .PageIsSettingsCollaboration}}class="current"{{end}}><a href="{{.RepoLink}}/settings/collaboration">{{.i18n.Tr "repo.settings.collaboration"}}</a></li>
+ <li {{if .PageIsSettingsBranches}}class="current"{{end}}><a href="{{.RepoLink}}/settings/branches">{{.i18n.Tr "repo.settings.branches"}}</a></li>
<li {{if .PageIsSettingsHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks">{{.i18n.Tr "repo.settings.hooks"}}</a></li>
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}}
<li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.i18n.Tr "repo.settings.githooks"}}</a></li>
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 7ebf2886c9..2c71557426 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -7,6 +7,11 @@
<a class="{{if .PageIsSettingsCollaboration}}active{{end}} item" href="{{.RepoLink}}/settings/collaboration">
{{.i18n.Tr "repo.settings.collaboration"}}
</a>
+ {{if not .Repository.IsBare}}
+ <a class="{{if .PageIsSettingsBranches}}active{{end}} item" href="{{.RepoLink}}/settings/branches">
+ {{.i18n.Tr "repo.settings.branches"}}
+ </a>
+ {{end}}
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks">
{{.i18n.Tr "repo.settings.hooks"}}
</a>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 363fb0527a..091828bc00 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -17,30 +17,6 @@
<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" autofocus required>
</div>
- <div class="field {{if .Err_Description}}error{{end}}">
- <label for="description">{{$.i18n.Tr "repo.repo_desc"}}</label>
- <textarea id="description" name="description" rows="2">{{.Repository.Description}}</textarea>
- </div>
- <div class="field {{if .Err_Website}}error{{end}}">
- <label for="website">{{.i18n.Tr "repo.settings.site"}}</label>
- <input id="website" name="website" type="url" value="{{.Repository.Website}}">
- </div>
-
- {{if not .Repository.IsBare}}
- <div class="required inline field">
- <label>{{.i18n.Tr "repo.default_branch"}}</label>
- <div class="ui selection dropdown">
- <input type="hidden" id="branch" name="branch" value="{{.Repository.DefaultBranch}}">
- <div class="text">{{.Repository.DefaultBranch}}</div>
- <i class="dropdown icon"></i>
- <div class="menu">
- {{range .Branches}}
- <div class="item" data-value="{{.}}">{{.}}</div>
- {{end}}
- </div>
- </div>
- </div>
- {{end}}
{{if not .Repository.IsFork}}
<div class="inline field">
<label>{{.i18n.Tr "repo.visibility"}}</label>
@@ -50,6 +26,14 @@
</div>
</div>
{{end}}
+ <div class="field {{if .Err_Description}}error{{end}}">
+ <label for="description">{{$.i18n.Tr "repo.repo_desc"}}</label>
+ <textarea id="description" name="description" rows="2">{{.Repository.Description}}</textarea>
+ </div>
+ <div class="field {{if .Err_Website}}error{{end}}">
+ <label for="website">{{.i18n.Tr "repo.settings.site"}}</label>
+ <input id="website" name="website" type="url" value="{{.Repository.Website}}">
+ </div>
<div class="field">
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>