diff options
-rw-r--r-- | cmd/serve.go | 4 | ||||
-rw-r--r-- | cmd/update.go | 20 | ||||
-rw-r--r-- | cmd/web.go | 5 | ||||
-rw-r--r-- | models/branches.go | 161 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v17.go | 29 | ||||
-rw-r--r-- | models/repo.go | 6 | ||||
-rw-r--r-- | modules/auth/repo_form.go | 1 | ||||
-rw-r--r-- | options/locale/TRANSLATORS | 4 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 12 | ||||
-rw-r--r-- | public/js/index.js | 37 | ||||
-rw-r--r-- | routers/repo/http.go | 97 | ||||
-rw-r--r-- | routers/repo/setting.go | 148 | ||||
-rw-r--r-- | templates/repo/settings/branches.tmpl | 91 | ||||
-rw-r--r-- | templates/repo/settings/nav.tmpl | 1 | ||||
-rw-r--r-- | templates/repo/settings/navbar.tmpl | 5 | ||||
-rw-r--r-- | templates/repo/settings/options.tmpl | 32 |
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> |