* 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 bugtags/v1.21.12.1
| @@ -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 | |||
| @@ -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], | |||
| @@ -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) | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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 | |||
| @@ -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(); | |||
| @@ -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 | |||
| @@ -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 { | |||
| @@ -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" .}} | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||