* 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
} 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 |
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" | ||||
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], |
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) |
// 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 | |||||
} |
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 |
// 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 | |||||
} |
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 |
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 |
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> | ||||
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> |
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 |
} | } | ||||
} | } | ||||
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'); | ||||
initEditForm(); | initEditForm(); | ||||
initEditor(); | initEditor(); | ||||
initOrganization(); | initOrganization(); | ||||
initProtectedBranch(); | |||||
initWebhook(); | initWebhook(); | ||||
initAdmin(); | initAdmin(); | ||||
initCodeView(); | initCodeView(); |
} 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 | ||||
} | } | ||||
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) | ||||
} | } | ||||
} | } | ||||
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) | ||||
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) | ||||
} | } | ||||
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 | ||||
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. | ||||
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 |
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" | ||||
// 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 | ||||
}) | }) | ||||
} | } | ||||
// 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 { |
{{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" .}} |
<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> |
<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> |
<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> | ||||
</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> |