* 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 { | } else { | ||||
| gitcmd = exec.Command(verb, repoPath) | 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.Dir = setting.RepoRootPath | ||||
| gitcmd.Stdout = os.Stdout | gitcmd.Stdout = os.Stdout | ||||
| gitcmd.Stdin = os.Stdin | gitcmd.Stdin = os.Stdin | ||||
| @@ -6,9 +6,12 @@ package cmd | |||||
| import ( | import ( | ||||
| "os" | "os" | ||||
| "strconv" | |||||
| "strings" | |||||
| "github.com/urfave/cli" | "github.com/urfave/cli" | ||||
| "code.gitea.io/git" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/setting" | "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") | 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{ | task := models.UpdateTask{ | ||||
| UUID: os.Getenv("GITEA_UUID"), | UUID: os.Getenv("GITEA_UUID"), | ||||
| RefName: args[0], | RefName: args[0], | ||||
| @@ -421,6 +421,11 @@ func runWeb(ctx *cli.Context) error { | |||||
| m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | ||||
| m.Post("/delete", repo.DeleteCollaboration) | 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.Group("/hooks", func() { | ||||
| m.Get("", repo.Webhooks) | 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), | NewMigration("create user column allow create organization", createAllowCreateOrganizationColumn), | ||||
| // V16 -> v17 | // V16 -> v17 | ||||
| NewMigration("create repo unit table and add units for all repos", addUnitsToTables), | 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 | // 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 | 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 | // IsOwnedBy returns true when user owns this repository | ||||
| func (repo *Repository) IsOwnedBy(userID int64) bool { | func (repo *Repository) IsOwnedBy(userID int64) bool { | ||||
| return repo.OwnerID == userID | return repo.OwnerID == userID | ||||
| @@ -88,7 +88,6 @@ type RepoSettingForm struct { | |||||
| RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` | ||||
| Description string `binding:"MaxSize(255)"` | Description string `binding:"MaxSize(255)"` | ||||
| Website string `binding:"Url;MaxSize(255)"` | Website string `binding:"Url;MaxSize(255)"` | ||||
| Branch string | |||||
| Interval int | Interval int | ||||
| MirrorAddress string | MirrorAddress string | ||||
| Private bool | Private bool | ||||
| @@ -49,10 +49,12 @@ Muhammad Fawwaz Orabi <mfawwaz93 AT gmail DOT com> | |||||
| Nakao Takamasa <at.mattenn AT gmail DOT com> | Nakao Takamasa <at.mattenn AT gmail DOT com> | ||||
| Natan Albuquerque <natanalbuquerque5 AT gmail DOT com> | Natan Albuquerque <natanalbuquerque5 AT gmail DOT com> | ||||
| Odilon Junior <odilon DOT junior93 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> | Richard Bukovansky <richard DOT bukovansky @ gmail DOT com> | ||||
| Robert Nuske <robert DOT nuske AT web DOT de> | Robert Nuske <robert DOT nuske AT web DOT de> | ||||
| Robin Hübner <profan AT prfn DOT se> | Robin Hübner <profan AT prfn DOT se> | ||||
| SeongJae Park <sj38 DOT park AT gmail DOT com> | 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> | Thomas Fanninger <gogs DOT thomas AT fanninger DOT at> | ||||
| Tilmann Bach <tilmann AT outlook DOT com> | Tilmann Bach <tilmann AT outlook DOT com> | ||||
| Toni Villena Jiménez <tonivj5 AT gmail 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> | Vladimir Vissoultchev <wqweto AT gmail DOT com> | ||||
| YJSoft <yjsoft AT yjsoft DOT pe DOT kr> | YJSoft <yjsoft AT yjsoft DOT pe DOT kr> | ||||
| Łukasz Jan Niemier <lukasz AT niemier DOT pl> | Ł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 = 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_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.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.browse_source = Browse Source | ||||
| diff.parent = parent | 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() { | function initRepositoryCollaboration() { | ||||
| console.log('initRepositoryCollaboration'); | console.log('initRepositoryCollaboration'); | ||||
| @@ -1402,6 +1438,7 @@ $(document).ready(function () { | |||||
| initEditForm(); | initEditForm(); | ||||
| initEditor(); | initEditor(); | ||||
| initOrganization(); | initOrganization(); | ||||
| initProtectedBranch(); | |||||
| initWebhook(); | initWebhook(); | ||||
| initAdmin(); | initAdmin(); | ||||
| initCodeView(); | initCodeView(); | ||||
| @@ -42,10 +42,20 @@ func HTTP(ctx *context.Context) { | |||||
| } else if service == "git-upload-pack" || | } else if service == "git-upload-pack" || | ||||
| strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { | strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { | ||||
| isPull = true | isPull = true | ||||
| } else if service == "git-upload-archive" || | |||||
| strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") { | |||||
| isPull = true | |||||
| } else { | } else { | ||||
| isPull = (ctx.Req.Method == "GET") | isPull = (ctx.Req.Method == "GET") | ||||
| } | } | ||||
| var accessMode models.AccessMode | |||||
| if isPull { | |||||
| accessMode = models.AccessModeRead | |||||
| } else { | |||||
| accessMode = models.AccessModeWrite | |||||
| } | |||||
| isWiki := false | isWiki := false | ||||
| if strings.HasSuffix(reponame, ".wiki") { | if strings.HasSuffix(reponame, ".wiki") { | ||||
| isWiki = true | isWiki = true | ||||
| @@ -146,17 +156,12 @@ func HTTP(ctx *context.Context) { | |||||
| } | } | ||||
| if !isPublicPull { | 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 { | if err != nil { | ||||
| ctx.Handle(http.StatusInternalServerError, "HasAccess", err) | ctx.Handle(http.StatusInternalServerError, "HasAccess", err) | ||||
| return | return | ||||
| } else if !has { | } else if !has { | ||||
| if tp == models.AccessModeRead { | |||||
| if accessMode == models.AccessModeRead { | |||||
| has, err = models.HasAccess(authUser, repo, models.AccessModeWrite) | has, err = models.HasAccess(authUser, repo, models.AccessModeWrite) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.Handle(http.StatusInternalServerError, "HasAccess2", err) | 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{ | HTTPBackend(ctx, &serviceConfig{ | ||||
| UploadPack: true, | UploadPack: true, | ||||
| ReceivePack: true, | ReceivePack: true, | ||||
| Params: params, | |||||
| OnSucceed: callback, | OnSucceed: callback, | ||||
| })(ctx.Resp, ctx.Req.Request) | })(ctx.Resp, ctx.Req.Request) | ||||
| @@ -244,6 +260,7 @@ func HTTP(ctx *context.Context) { | |||||
| type serviceConfig struct { | type serviceConfig struct { | ||||
| UploadPack bool | UploadPack bool | ||||
| ReceivePack bool | ReceivePack bool | ||||
| Params map[string]string | |||||
| OnSucceed func(rpc string, input []byte) | 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") | 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() { | func (h *serviceHandler) setHeaderCacheForever() { | ||||
| now := time.Now().Unix() | now := time.Now().Unix() | ||||
| expires := now + 31536000 | expires := now + 31536000 | ||||
| @@ -358,13 +411,15 @@ func serviceRPC(h serviceHandler, service string) { | |||||
| h.w.WriteHeader(http.StatusUnauthorized) | h.w.WriteHeader(http.StatusUnauthorized) | ||||
| return | return | ||||
| } | } | ||||
| h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) | h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) | ||||
| var ( | 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. | // Handle GZIP. | ||||
| @@ -385,11 +440,31 @@ func serviceRPC(h serviceHandler, service string) { | |||||
| return | return | ||||
| } | } | ||||
| branchName = h.getBranch(input) | |||||
| br = bytes.NewReader(input) | br = bytes.NewReader(input) | ||||
| } else { | } else { | ||||
| br = reqBody | 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 := exec.Command("git", service, "--stateless-rpc", h.dir) | ||||
| cmd.Dir = h.dir | cmd.Dir = h.dir | ||||
| cmd.Stdout = h.w | cmd.Stdout = h.w | ||||
| @@ -21,6 +21,7 @@ import ( | |||||
| const ( | const ( | ||||
| tplSettingsOptions base.TplName = "repo/settings/options" | tplSettingsOptions base.TplName = "repo/settings/options" | ||||
| tplCollaboration base.TplName = "repo/settings/collaboration" | tplCollaboration base.TplName = "repo/settings/collaboration" | ||||
| tplBranches base.TplName = "repo/settings/branches" | |||||
| tplGithooks base.TplName = "repo/settings/githooks" | tplGithooks base.TplName = "repo/settings/githooks" | ||||
| tplGithookEdit base.TplName = "repo/settings/githook_edit" | tplGithookEdit base.TplName = "repo/settings/githook_edit" | ||||
| tplDeployKeys base.TplName = "repo/settings/deploy_keys" | 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. | // In case it's just a case change. | ||||
| repo.Name = newRepoName | repo.Name = newRepoName | ||||
| repo.LowerName = strings.ToLower(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.Description = form.Description | ||||
| repo.Website = form.Website | 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) { | func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | ||||
| owner, err := models.GetUserByName(ctx.Params(":username")) | owner, err := models.GetUserByName(ctx.Params(":username")) | ||||
| if err != nil { | 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"> | <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 .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 .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> | <li {{if .PageIsSettingsHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks">{{.i18n.Tr "repo.settings.hooks"}}</a></li> | ||||
| {{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}} | {{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> | <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"> | <a class="{{if .PageIsSettingsCollaboration}}active{{end}} item" href="{{.RepoLink}}/settings/collaboration"> | ||||
| {{.i18n.Tr "repo.settings.collaboration"}} | {{.i18n.Tr "repo.settings.collaboration"}} | ||||
| </a> | </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"> | <a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks"> | ||||
| {{.i18n.Tr "repo.settings.hooks"}} | {{.i18n.Tr "repo.settings.hooks"}} | ||||
| </a> | </a> | ||||
| @@ -17,30 +17,6 @@ | |||||
| <label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label> | <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> | <input id="repo_name" name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" autofocus required> | ||||
| </div> | </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}} | {{if not .Repository.IsFork}} | ||||
| <div class="inline field"> | <div class="inline field"> | ||||
| <label>{{.i18n.Tr "repo.visibility"}}</label> | <label>{{.i18n.Tr "repo.visibility"}}</label> | ||||
| @@ -50,6 +26,14 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {{end}} | {{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"> | <div class="field"> | ||||
| <button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button> | <button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button> | ||||