* 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.1.0
@@ -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> |