Browse Source

Initial version of protected branches (#776)

- Able to restrict force push and deletion
- Able to restrict direct push
pull/4150/head
Unknwon 8 years ago
parent
commit
7e09d210ba
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
  1. 4
      README.md
  2. 2
      README_ZH.md
  3. 48
      cmd/hook.go
  4. 11
      cmd/serv.go
  5. 29
      cmd/web.go
  6. 16
      conf/locale/locale_en-US.ini
  7. 2
      gogs.go
  8. 1
      models/action.go
  9. 4
      models/models.go
  10. 4
      models/repo.go
  11. 57
      models/repo_branch.go
  12. 16
      modules/auth/repo_form.go
  13. 6
      modules/bindata/bindata.go
  14. 2
      modules/context/repo.go
  15. 19
      public/css/gogs.css
  16. 12
      public/js/gogs.js
  17. 28
      public/less/_repository.less
  18. 50
      routers/repo/http.go
  19. 16
      routers/repo/pull.go
  20. 148
      routers/repo/setting.go
  21. 13
      routers/repo/view.go
  22. 2
      templates/.VERSION
  23. 62
      templates/repo/settings/branches.tmpl
  24. 5
      templates/repo/settings/navbar.tmpl
  25. 15
      templates/repo/settings/options.tmpl
  26. 53
      templates/repo/settings/protected_branch.tmpl

4
README.md

@ -1,4 +1,4 @@
Gogs [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs) [![Build status](https://ci.appveyor.com/api/projects/status/b9uu5ejl933e2wlt/branch/master?svg=true)](https://ci.appveyor.com/project/Unknwon/gogs/branch/master) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/gogs/localized.svg)](https://crowdin.com/project/gogs) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gogits/gogs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Gogs [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs) [![Build status](https://ci.appveyor.com/api/projects/status/b9uu5ejl933e2wlt/branch/master?svg=true)](https://ci.appveyor.com/project/Unknwon/gogs/branch/master) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/gogs/localized.svg)](https://crowdin.com/project/gogs) [![Sourcegraph](https://sourcegraph.com/github.com/gogits/gogs/-/badge.svg)](https://sourcegraph.com/github.com/gogits/gogs?badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gogits/gogs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
=====================
![](https://github.com/gogits/gogs/blob/master/public/img/gogs-large-resize.png?raw=true)
@ -43,7 +43,7 @@ The goal of this project is to make the easiest, fastest, and most painless way
- Add/Remove repository collaborators
- Repository/Organization webhooks (including Slack)
- Repository Git hooks/deploy keys
- Repository issues, pull requests and wiki
- Repository issues, pull requests, wiki and protected branches
- Migrate and mirror repository and its wiki
- Web editor for repository files and wiki
- Jupyter Notebook

2
README_ZH.md

@ -24,7 +24,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
- 支持添加和删除仓库协作者
- 支持仓库和组织级别 Web 钩子(包括 Slack 集成)
- 支持仓库 Git 钩子和部署密钥
- 支持仓库工单(Issue)、合并请求(Pull Request)以及 Wiki
- 支持仓库工单(Issue)、合并请求(Pull Request)、Wiki 和保护分支
- 支持迁移和镜像仓库以及它的 Wiki
- 支持在线编辑仓库文件和 Wiki
- 支持自定义源的 Gravatar 和 Federated Avatar

48
cmd/hook.go

@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"crypto/tls"
"fmt"
"os"
"os/exec"
"path/filepath"
@ -64,13 +65,58 @@ func runHookPreReceive(c *cli.Context) error {
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
return nil
}
setup(c, "hooks/pre-receive.log", false)
setup(c, "hooks/pre-receive.log", true)
isWiki := strings.Contains(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
buf.Write(scanner.Bytes())
buf.WriteByte('\n')
if isWiki {
continue
}
fields := bytes.Fields(scanner.Bytes())
if len(fields) != 3 {
continue
}
oldCommitID := string(fields[0])
newCommitID := string(fields[1])
branchName := strings.TrimPrefix(string(fields[2]), git.BRANCH_PREFIX)
// Branch protection
repoID := com.StrTo(os.Getenv(http.ENV_REPO_ID)).MustInt64()
protectBranch, err := models.GetProtectBranchOfRepoByName(repoID, branchName)
if err != nil {
if models.IsErrBranchNotExist(err) {
continue
}
fail("Internal error", "GetProtectBranchOfRepoByName [repo_id: %d, branch: %s]: %v", repoID, branchName, err)
}
if !protectBranch.Protected {
continue
}
// Check if branch allows direct push
if protectBranch.RequirePullRequest {
fail(fmt.Sprintf("Branch '%s' is protected and commits must be merged through pull request", branchName), "")
}
// check and deletion
if newCommitID == git.EMPTY_SHA {
fail(fmt.Sprintf("Branch '%s' is protected from deletion", branchName), "")
}
// Check force push
output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).Run()
if err != nil {
fail("Internal error", "Fail to detect force push: %v", err)
} else if len(output) > 0 {
fail(fmt.Sprintf("Branch '%s' is protected from force push", branchName), "")
}
}
customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive")

11
cmd/serv.go

@ -175,7 +175,7 @@ func runServ(c *cli.Context) error {
// Prohibit push to mirror repositories.
if requestMode > models.ACCESS_MODE_READ && repo.IsMirror {
fail("mirror repository is read-only", "")
fail("Mirror repository is read-only", "")
}
// Allow anonymous (user is nil) clone for public repositories.
@ -251,7 +251,14 @@ func runServ(c *cli.Context) error {
gitCmd = exec.Command(verb, repoFullName)
}
if requestMode == models.ACCESS_MODE_WRITE {
gitCmd.Env = append(os.Environ(), http.ComposeHookEnvs(repo.RepoPath(), owner.Name, owner.Salt, repo.Name, user)...)
gitCmd.Env = append(os.Environ(), http.ComposeHookEnvs(http.ComposeHookEnvsOptions{
AuthUser: user,
OwnerName: owner.Name,
OwnerSalt: owner.Salt,
RepoID: repo.ID,
RepoName: repo.Name,
RepoPath: repo.RepoPath(),
})...)
}
gitCmd.Dir = setting.RepoRootPath
gitCmd.Stdout = os.Stdout

29
cmd/web.go

@ -435,10 +435,21 @@ func runWeb(ctx *cli.Context) error {
m.Combo("").Get(repo.Settings).
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
m.Group("/collaboration", func() {
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
m.Post("/delete", repo.DeleteCollaboration)
})
m.Group("/branches", func() {
m.Get("", repo.SettingsBranches)
m.Post("/default_branch", repo.UpdateDefaultBranch)
m.Combo("/*").Get(repo.SettingsProtectedBranch).
Post(bindIgnErr(auth.ProtectBranchForm{}), repo.SettingsProtectedBranchPost)
}, func(ctx *context.Context) {
if ctx.Repo.Repository.IsMirror {
ctx.NotFound()
return
}
})
m.Group("/hooks", func() {
m.Get("", repo.Webhooks)
@ -452,15 +463,15 @@ func runWeb(ctx *cli.Context) error {
m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
m.Group("/git", func() {
m.Get("", repo.GitHooks)
m.Combo("/:name").Get(repo.GitHooksEdit).
Post(repo.GitHooksEditPost)
m.Get("", repo.SettingsGitHooks)
m.Combo("/:name").Get(repo.SettingsGitHooksEdit).
Post(repo.SettingsGitHooksEditPost)
}, context.GitHookService())
})
m.Group("/keys", func() {
m.Combo("").Get(repo.DeployKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.DeployKeysPost)
m.Combo("").Get(repo.SettingsDeployKeys).
Post(bindIgnErr(auth.AddSSHKeyForm{}), repo.SettingsDeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
})
@ -555,13 +566,13 @@ func runWeb(ctx *cli.Context) error {
m.Post("/upload-remove", bindIgnErr(auth.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
}, func(ctx *context.Context) {
if !setting.Repository.Upload.Enabled {
ctx.Handle(404, "", nil)
ctx.NotFound()
return
}
})
}, reqRepoWriter, context.RepoRef(), func(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
ctx.Handle(404, "", nil)
if !ctx.Repo.CanEnableEditor() {
ctx.NotFound()
return
}
})

16
conf/locale/locale_en-US.ini

@ -639,6 +639,22 @@ settings.collaboration.admin = Admin
settings.collaboration.write = Write
settings.collaboration.read = Read
settings.collaboration.undefined = Undefined
settings.branches = Branches
settings.default_branch = Default Branch
settings.default_branch_desc = The default branch is considered the "base" branch for code commits, pull requests and online editing.
settings.update = Update
settings.update_default_branch_success = Default branch of this repository has been updated successfully!
settings.protected_branches = Protected Branches
settings.protected_branches_desc = Protect branches from force pushing, accidental deletion and whitelist code committers.
settings.choose_a_branch = Choose a branch...
settings.branch_protection = Branch Protection
settings.branch_protection_desc = Please choose protect options for branch <b>%s</b>.
settings.protect_this_branch = Protect this branch
settings.protect_this_branch_desc = Disable force pushes and prevent from deletion.
settings.protect_require_pull_request = Require pull request instead direct pushing
settings.protect_require_pull_request_desc = Enable this option to disable direct pushing to this branch. Commits have to be pushed to another non-protected branch and merged to this branch through pull request.
settings.protect_whitelist_committers = Whitelist who can push to this branch
settings.protect_whitelist_committers_desc = Add people or teams to whitelist of direct push to this branch.
settings.hooks = Webhooks
settings.githooks = Git Hooks
settings.basic_settings = Basic Settings

2
gogs.go

@ -16,7 +16,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
const APP_VER = "0.9.153.0217"
const APP_VER = "0.9.154.0217"
func init() {
setting.AppVer = APP_VER

1
models/action.go

@ -460,6 +460,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
opType = ACTION_PUSH_TAG
opts.Commits = &PushCommits{}
} else {
// TODO: detect branch deletion
// if not the first commit, set the compare URL.
if opts.OldCommitID == git.EMPTY_SHA {
isNewBranch = true

4
models/models.go

@ -65,8 +65,8 @@ func init() {
new(Watch), new(Star), new(Follow), new(Action),
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
new(Label), new(IssueLabel), new(Milestone),
new(Mirror), new(Release), new(LoginSource), new(Webhook),
new(HookTask),
new(Mirror), new(Release), new(LoginSource), new(Webhook), new(HookTask),
new(ProtectBranch),
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
new(Notice), new(EmailAddress))

4
models/repo.go

@ -441,6 +441,10 @@ func (repo *Repository) AllowsPulls() bool {
return repo.CanEnablePulls() && repo.EnablePulls
}
func (repo *Repository) IsBranchRequirePullRequest(name string) bool {
return IsBranchOfRepoRequirePullRequest(repo.ID, name)
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
func (repo *Repository) CanEnableEditor() bool {
return !repo.IsMirror

57
models/repo_branch.go

@ -5,6 +5,8 @@
package models
import (
"fmt"
"github.com/gogits/git-module"
)
@ -36,7 +38,7 @@ func GetBranchesByPath(path string) ([]*Branch, error) {
func (repo *Repository) GetBranch(br string) (*Branch, error) {
if !git.IsBranchExist(repo.RepoPath(), br) {
return nil, &ErrBranchNotExist{br}
return nil, ErrBranchNotExist{br}
}
return &Branch{
Path: repo.RepoPath(),
@ -55,3 +57,56 @@ func (br *Branch) GetCommit() (*git.Commit, error) {
}
return gitRepo.GetBranchCommit(br.Name)
}
// ProtectBranch contains options of a protected branch.
type ProtectBranch struct {
ID int64
RepoID int64 `xorm:"UNIQUE(protect_branch)"`
Name string `xorm:"UNIQUE(protect_branch)"`
Protected bool
RequirePullRequest bool
}
// GetProtectBranchOfRepoByName returns *ProtectBranch by branch name in given repostiory.
func GetProtectBranchOfRepoByName(repoID int64, name string) (*ProtectBranch, error) {
protectBranch := &ProtectBranch{
RepoID: repoID,
Name: name,
}
has, err := x.Get(protectBranch)
if err != nil {
return nil, err
} else if !has {
return nil, ErrBranchNotExist{name}
}
return protectBranch, nil
}
// IsBranchOfRepoRequirePullRequest returns true if branch requires pull request in given repository.
func IsBranchOfRepoRequirePullRequest(repoID int64, name string) bool {
protectBranch, err := GetProtectBranchOfRepoByName(repoID, name)
if err != nil {
return false
}
return protectBranch.Protected && protectBranch.RequirePullRequest
}
// UpdateProtectBranch saves branch protection options.
// If ID is 0, it creates a new record. Otherwise, updates existing record.
func UpdateProtectBranch(protectBranch *ProtectBranch) (err error) {
if protectBranch.ID == 0 {
if _, err = x.Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %v", err)
}
return
}
_, err = x.Id(protectBranch.ID).AllCols().Update(protectBranch)
return err
}
// GetProtectBranchesByRepoID returns a list of *ProtectBranch in given repostiory.
func GetProtectBranchesByRepoID(repoID int64) ([]*ProtectBranch, error) {
protectBranches := make([]*ProtectBranch, 0, 2)
return protectBranches, x.Where("repo_id = ?", repoID).Asc("name").Find(&protectBranches)
}

16
modules/auth/repo_form.go

@ -106,6 +106,22 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
return validate(errs, ctx.Data, f, ctx.Locale)
}
// __________ .__
// \______ \____________ ____ ____ | |__
// | | _/\_ __ \__ \ / \_/ ___\| | \
// | | \ | | \// __ \| | \ \___| Y \
// |______ / |__| (____ /___| /\___ >___| /
// \/ \/ \/ \/ \/
type ProtectBranchForm struct {
Protected bool
RequirePullRequest bool
}
func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
// __ __ ___. .__ .__ __
// / \ / \ ____\_ |__ | |__ | |__ ____ | | __
// \ \/\/ // __ \| __ \| | \| | \ / _ \| |/ /

6
modules/bindata/bindata.go

File diff suppressed because one or more lines are too long

2
modules/context/repo.go

@ -73,7 +73,7 @@ func (r *Repository) HasAccess() bool {
// CanEnableEditor returns true if repository is editable and user has proper access level.
func (r *Repository) CanEnableEditor() bool {
return r.Repository.CanEnableEditor() && r.IsViewBranch && r.IsWriter()
return r.Repository.CanEnableEditor() && r.IsViewBranch && r.IsWriter() && !r.Repository.IsBranchRequirePullRequest(r.BranchName)
}
// GetEditorconfig returns the .editorconfig definition if found in the

19
public/css/gogs.css

@ -1225,7 +1225,6 @@ footer .ui.language .menu {
}
.repository.file.list #file-buttons {
font-weight: normal;
margin-top: -3px;
}
.repository.file.list #file-buttons .ui.button {
padding: 8px 10px;
@ -2274,6 +2273,24 @@ footer .ui.language .menu {
margin-left: 5px;
margin-top: -3px;
}
.repository.settings.branches .protected-branches .selection.dropdown {
width: 300px;
}
.repository.settings.branches .protected-branches .item {
border: 1px solid #eaeaea;
padding: 10px 15px;
}
.repository.settings.branches .protected-branches .item:not(:last-child) {
border-bottom: 0;
}
.repository.settings.branches .branch-protection .help {
margin-left: 26px;
padding-top: 0;
}
.repository.settings.branches .branch-protection .fields {
margin-left: 20px;
display: block;
}
.repository.settings.webhook .events .column {
padding-bottom: 0;
}

12
public/js/gogs.js

@ -341,6 +341,18 @@ function initRepository() {
});
}
// Branches
if ($('.repository.settings.branches').length > 0) {
initFilterSearchDropdown('.protected-branches .dropdown');
$('.enable-protection').change(function () {
if (this.checked) {
$($(this).data('target')).removeClass('disabled');
} else {
$($(this).data('target')).addClass('disabled');
}
});
}
// Labels
if ($('.repository.labels').length > 0) {
// Create label

28
public/less/_repository.less

@ -161,7 +161,7 @@
}
#file-buttons {
font-weight: normal;
margin-top: -3px;
.ui.button {
padding: 8px 10px;
font-weight: normal;
@ -1303,6 +1303,32 @@
}
}
&.branches {
.protected-branches {
.selection.dropdown {
width: 300px;
}
.item {
border: 1px solid #eaeaea;
padding: 10px 15px;
&:not(:last-child) {
border-bottom: 0;
}
}
}
.branch-protection {
.help {
margin-left: 26px;
padding-top: 0;
}
.fields {
margin-left: 20px;
display: block;
}
}
}
&.webhook {
.events {
.column {

50
routers/repo/http.go

@ -28,18 +28,20 @@ import (
)
const (
ENV_AUTH_USER_ID = "AUTH_USER_ID"
ENV_AUTH_USER_NAME = "AUTH_USER_NAME"
ENV_REPO_OWNER_NAME = "REPO_OWNER_NAME"
ENV_REPO_OWNER_SALT_MD5 = "REPO_OWNER_SALT_MD5"
ENV_REPO_NAME = "REPO_NAME"
ENV_REPO_CUSTOM_HOOKS_PATH = "REPO_CUSTOM_HOOKS_PATH"
ENV_AUTH_USER_ID = "GOGS_AUTH_USER_ID"
ENV_AUTH_USER_NAME = "GOGS_AUTH_USER_NAME"
ENV_REPO_OWNER_NAME = "GOGS_REPO_OWNER_NAME"
ENV_REPO_OWNER_SALT_MD5 = "GOGS_REPO_OWNER_SALT_MD5"
ENV_REPO_ID = "GOGS_REPO_ID"
ENV_REPO_NAME = "GOGS_REPO_NAME"
ENV_REPO_CUSTOM_HOOKS_PATH = "GOGS_REPO_CUSTOM_HOOKS_PATH"
)
type HTTPContext struct {
*context.Context
OwnerName string
OwnerSalt string
RepoID int64
RepoName string
AuthUser *models.User
}
@ -143,6 +145,7 @@ func HTTPContexter() macaron.Handler {
Context: ctx,
OwnerName: ownerName,
OwnerSalt: owner.Salt,
RepoID: repo.ID,
RepoName: repoName,
AuthUser: authUser,
})
@ -158,6 +161,7 @@ type serviceHandler struct {
authUser *models.User
ownerName string
ownerSalt string
repoID int64
repoName string
}
@ -189,15 +193,25 @@ func (h *serviceHandler) sendFile(contentType string) {
http.ServeFile(h.w, h.r, reqFile)
}
func ComposeHookEnvs(repoPath, ownerName, ownerSalt, repoName string, authUser *models.User) []string {
type ComposeHookEnvsOptions struct {
AuthUser *models.User
OwnerName string
OwnerSalt string
RepoID int64
RepoName string
RepoPath string
}
func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string {
envs := []string{
"SSH_ORIGINAL_COMMAND=1",
ENV_AUTH_USER_ID + "=" + com.ToStr(authUser.ID),
ENV_AUTH_USER_NAME + "=" + authUser.Name,
ENV_REPO_OWNER_NAME + "=" + ownerName,
ENV_REPO_OWNER_SALT_MD5 + "=" + base.EncodeMD5(ownerSalt),
ENV_REPO_NAME + "=" + repoName,
ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(repoPath, "custom_hooks"),
ENV_AUTH_USER_ID + "=" + com.ToStr(opts.AuthUser.ID),
ENV_AUTH_USER_NAME + "=" + opts.AuthUser.Name,
ENV_REPO_OWNER_NAME + "=" + opts.OwnerName,
ENV_REPO_OWNER_SALT_MD5 + "=" + base.EncodeMD5(opts.OwnerSalt),
ENV_REPO_ID + "=" + com.ToStr(opts.RepoID),
ENV_REPO_NAME + "=" + opts.RepoName,
ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(opts.RepoPath, "custom_hooks"),
}
return envs
}
@ -229,7 +243,14 @@ func serviceRPC(h serviceHandler, service string) {
var stderr bytes.Buffer
cmd := exec.Command("git", service, "--stateless-rpc", h.dir)
if service == "receive-pack" {
cmd.Env = append(os.Environ(), ComposeHookEnvs(h.dir, h.ownerName, h.ownerSalt, h.repoName, h.authUser)...)
cmd.Env = append(os.Environ(), ComposeHookEnvs(ComposeHookEnvsOptions{
AuthUser: h.authUser,
OwnerName: h.ownerName,
OwnerSalt: h.ownerSalt,
RepoID: h.repoID,
RepoName: h.repoName,
RepoPath: h.dir,
})...)
}
cmd.Dir = h.dir
cmd.Stdout = h.w
@ -392,6 +413,7 @@ func HTTP(ctx *HTTPContext) {
authUser: ctx.AuthUser,
ownerName: ctx.OwnerName,
ownerSalt: ctx.OwnerSalt,
repoID: ctx.RepoID,
repoName: ctx.RepoName,
})
return

16
routers/repo/pull.go

@ -711,6 +711,22 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pullIssue.Index))
}
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
if err != nil {
ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err)
return nil, nil
}
repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
if err != nil {
ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoNotExist, err)
return nil, nil
}
return owner, repo
}
func TriggerTask(ctx *context.Context) {
pusherID := ctx.QueryInt64("pusher")
branch := ctx.Query("branch")

148
routers/repo/setting.go

@ -5,6 +5,7 @@
package repo
import (
"fmt"
"strings"
"time"
@ -22,10 +23,12 @@ import (
const (
SETTINGS_OPTIONS base.TplName = "repo/settings/options"
COLLABORATION base.TplName = "repo/settings/collaboration"
GITHOOKS base.TplName = "repo/settings/githooks"
GITHOOK_EDIT base.TplName = "repo/settings/githook_edit"
DEPLOY_KEYS base.TplName = "repo/settings/deploy_keys"
SETTINGS_COLLABORATION base.TplName = "repo/settings/collaboration"
SETTINGS_BRANCHES base.TplName = "repo/settings/branches"
SETTINGS_PROTECTED_BRANCH base.TplName = "repo/settings/protected_branch"
SETTINGS_GITHOOKS base.TplName = "repo/settings/githooks"
SETTINGS_GITHOOK_EDIT base.TplName = "repo/settings/githook_edit"
SETTINGS_DEPLOY_KEYS base.TplName = "repo/settings/deploy_keys"
)
func Settings(ctx *context.Context) {
@ -74,16 +77,6 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
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
@ -295,7 +288,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
}
}
func Collaboration(ctx *context.Context) {
func SettingsCollaboration(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsCollaboration"] = true
@ -306,10 +299,10 @@ func Collaboration(ctx *context.Context) {
}
ctx.Data["Collaborators"] = users
ctx.HTML(200, COLLABORATION)
ctx.HTML(200, SETTINGS_COLLABORATION)
}
func CollaborationPost(ctx *context.Context) {
func SettingsCollaborationPost(ctx *context.Context) {
name := strings.ToLower(ctx.Query("collaborator"))
if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path)
@ -374,31 +367,102 @@ func DeleteCollaboration(ctx *context.Context) {
})
}
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
owner, err := models.GetUserByName(ctx.Params(":username"))
func SettingsBranches(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
ctx.Data["PageIsSettingsBranches"] = true
protectBranches, err := models.GetProtectBranchesByRepoID(ctx.Repo.Repository.ID)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.Handle(404, "GetUserByName", nil)
} else {
ctx.Handle(500, "GetUserByName", err)
ctx.Handle(500, "GetProtectBranchesByRepoID", err)
return
}
ctx.Data["ProtectBranches"] = protectBranches
ctx.HTML(200, SETTINGS_BRANCHES)
}
func UpdateDefaultBranch(ctx *context.Context) {
branch := ctx.Query("branch")
if ctx.Repo.GitRepo.IsBranchExist(branch) &&
ctx.Repo.Repository.DefaultBranch != branch {
ctx.Repo.Repository.DefaultBranch = branch
if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.Handle(500, "SetDefaultBranch", err)
return
}
}
}
if err := models.UpdateRepository(ctx.Repo.Repository, false); err != nil {
ctx.Handle(500, "UpdateRepository", err)
return
}
return nil, nil
ctx.Flash.Success(ctx.Tr("repo.settings.update_default_branch_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/branches")
}
func SettingsProtectedBranch(ctx *context.Context) {
branch := ctx.Params("*")
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
ctx.NotFound()
return
}
repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
ctx.Data["Title"] = ctx.Tr("repo.settings.protected_branches") + " - " + branch
ctx.Data["PageIsSettingsBranches"] = true
ctx.Data["IsOrgRepo"] = ctx.Repo.Owner.IsOrganization()
protectBranch, err := models.GetProtectBranchOfRepoByName(ctx.Repo.Repository.ID, branch)
if err != nil {
if models.IsErrRepoNotExist(err) {
ctx.Handle(404, "GetRepositoryByName", nil)
} else {
ctx.Handle(500, "GetRepositoryByName", err)
if !models.IsErrBranchNotExist(err) {
ctx.Handle(500, "GetProtectBranchOfRepoByName", err)
return
}
// No options found, create defaults.
protectBranch = &models.ProtectBranch{
Name: branch,
}
}
ctx.Data["Branch"] = protectBranch
ctx.HTML(200, SETTINGS_PROTECTED_BRANCH)
}
func SettingsProtectedBranchPost(ctx *context.Context, form auth.ProtectBranchForm) {
branch := ctx.Params("*")
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
ctx.NotFound()
return
}
protectBranch, err := models.GetProtectBranchOfRepoByName(ctx.Repo.Repository.ID, branch)
if err != nil {
if !models.IsErrBranchNotExist(err) {
ctx.Handle(500, "GetProtectBranchOfRepoByName", err)
return
}
// No options found, create defaults.
protectBranch = &models.ProtectBranch{
RepoID: ctx.Repo.Repository.ID,
Name: branch,
}
return nil, nil
}
return owner, repo
protectBranch.Protected = form.Protected
protectBranch.RequirePullRequest = form.RequirePullRequest
if err = models.UpdateProtectBranch(protectBranch); err != nil {
ctx.Handle(500, "UpdateProtectBranch", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
}
func GitHooks(ctx *context.Context) {
func SettingsGitHooks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
ctx.Data["PageIsSettingsGitHooks"] = true
@ -409,10 +473,10 @@ func GitHooks(ctx *context.Context) {
}
ctx.Data["Hooks"] = hooks
ctx.HTML(200, GITHOOKS)
ctx.HTML(200, SETTINGS_GITHOOKS)
}
func GitHooksEdit(ctx *context.Context) {
func SettingsGitHooksEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
ctx.Data["PageIsSettingsGitHooks"] = true
@ -427,10 +491,10 @@ func GitHooksEdit(ctx *context.Context) {
return
}
ctx.Data["Hook"] = hook
ctx.HTML(200, GITHOOK_EDIT)
ctx.HTML(200, SETTINGS_GITHOOK_EDIT)
}
func GitHooksEditPost(ctx *context.Context) {
func SettingsGitHooksEditPost(ctx *context.Context) {
name := ctx.Params(":name")
hook, err := ctx.Repo.GitRepo.GetHook(name)
if err != nil {
@ -449,7 +513,7 @@ func GitHooksEditPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
}
func DeployKeys(ctx *context.Context) {
func SettingsDeployKeys(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
@ -460,10 +524,10 @@ func DeployKeys(ctx *context.Context) {
}
ctx.Data["Deploykeys"] = keys
ctx.HTML(200, DEPLOY_KEYS)
ctx.HTML(200, SETTINGS_DEPLOY_KEYS)
}
func DeployKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
func SettingsDeployKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
@ -475,7 +539,7 @@ func DeployKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
ctx.Data["Deploykeys"] = keys
if ctx.HasError() {
ctx.HTML(200, DEPLOY_KEYS)
ctx.HTML(200, SETTINGS_DEPLOY_KEYS)
return
}
@ -498,10 +562,10 @@ func DeployKeysPost(ctx *context.Context, form auth.AddSSHKeyForm) {
switch {
case models.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), DEPLOY_KEYS, &form)
ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), SETTINGS_DEPLOY_KEYS, &form)
case models.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), DEPLOY_KEYS, &form)
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), SETTINGS_DEPLOY_KEYS, &form)
default:
ctx.Handle(500, "AddDeployKey", err)
}

13
routers/repo/view.go

@ -108,8 +108,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
// Check permission to add or upload new file.
if ctx.Repo.IsWriter() && ctx.Repo.IsViewBranch {
if ctx.Repo.CanEnableEditor() {
ctx.Data["CanAddFile"] = true
ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled
}
@ -142,6 +141,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
}
canEnableEditor := ctx.Repo.CanEnableEditor()
switch {
case isTextFile:
if blob.Size() >= setting.UI.MaxDisplayFileSize {
@ -186,7 +186,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
}
if ctx.Repo.CanEnableEditor() {
if canEnableEditor {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
} else if !ctx.Repo.IsViewBranch {
@ -203,7 +203,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["IsImageFile"] = true
}
if ctx.Repo.CanEnableEditor() {
if canEnableEditor {
ctx.Data["CanDeleteFile"] = true
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
} else if !ctx.Repo.IsViewBranch {
@ -216,7 +216,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
func setEditorconfigIfExists(ctx *context.Context) {
ec, err := ctx.Repo.GetEditorconfig()
if err != nil && !git.IsErrNotExist(err) {
log.Error(4, "Fail to get '.editorconfig' [%d]: %v", ctx.Repo.Repository.ID, err)
log.Trace("setEditorconfigIfExists.GetEditorconfig [%d]: %v", ctx.Repo.Repository.ID, err)
return
}
ctx.Data["Editorconfig"] = ec
@ -228,6 +228,9 @@ func Home(ctx *context.Context) {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
if ctx.Repo.BranchName != ctx.Repo.Repository.DefaultBranch {
ctx.Data["Title"] = title + " @ " + ctx.Repo.BranchName
}
ctx.Data["PageIsViewCode"] = true
ctx.Data["RequireHighlightJS"] = true

2
templates/.VERSION

@ -1 +1 @@
0.9.153.0217
0.9.154.0217

62
templates/repo/settings/branches.tmpl

@ -0,0 +1,62 @@
{{template "base/head" .}}
<div class="repository settings branches">
{{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.settings.default_branch"}}
</h4>
<div class="ui attached segment default-branch">
<p>{{.i18n.Tr "repo.settings.default_branch_desc"}}</p>
<form class="ui form" action="{{.Link}}/default_branch" method="post">
{{.CsrfTokenHtml}}
<div class="required inline field {{if .Repository.IsBare}}disabled{{end}}">
<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>
<button class="ui green button">{{$.i18n.Tr "repo.settings.update"}}</button>
</div>
</form>
</div>
<h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.protected_branches"}}
</h4>
<div class="ui attached segment protected-branches">
<p>{{.i18n.Tr "repo.settings.protected_branches_desc"}}</p>
<div class="ui form">
<div class="required inline field {{if .Repository.IsBare}}disabled{{end}}">
<div class="ui selection dropdown">
<div class="text">{{.i18n.Tr "repo.settings.choose_a_branch"}}</div>
<i class="dropdown icon"></i>
<div class="menu">
{{range .Branches}}
<div class="item" data-url="{{$.Link}}/{{.}}">{{.}}</div>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui protected-branches list">
{{range .ProtectBranches}}
<div class="item">
<a href="{{$.Link}}/{{.Name}}"><code>{{.Name}}</code></a>
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

5
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.IsMirror}}
<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>

15
templates/repo/settings/options.tmpl

@ -26,21 +26,6 @@
<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>

53
templates/repo/settings/protected_branch.tmpl

@ -0,0 +1,53 @@
{{template "base/head" .}}
<div class="repository settings branches">
{{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.settings.branch_protection"}}
</h4>
<div class="ui attached segment branch-protection">
<p>{{.i18n.Tr "repo.settings.branch_protection_desc" .Branch.Name | Str2html}}</p>
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="inline field">
<div class="ui checkbox">
<input class="enable-protection" name="protected" type="checkbox" data-target="#protection_box" {{if .Branch.Protected}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.protect_this_branch"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.protect_this_branch_desc"}}</p>
</div>
</div>
<div id="protection_box" class="fields {{if not .Branch.Protected}}disabled{{end}}">
<div class="field">
<div class="ui checkbox">
<input name="require_pull_request" type="checkbox" {{if .Branch.RequirePullRequest}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.protect_require_pull_request"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.protect_require_pull_request_desc"}}</p>
</div>
</div>
{{if .IsOrgRepo}}
<div class="field">
<div class="ui checkbox">
<input name="whitelist_committers" type="checkbox" data-target="#whitelist_box">
<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
</div>
</div>
{{end}}
</div>
<div class="ui divider"></div>
<div class="field">
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
Loading…
Cancel
Save