* improve protected branch to add whitelist support * fix lint * fix style check * fix tests * fix description on UI and import * fix test * bug fixed * fix tests and languages * move isSliceInt64Eq to util pkg; improve function names & typotags/v1.3.0-rc1
@@ -84,9 +84,10 @@ func runHookPreReceive(c *cli.Context) error { | |||
// the environment setted on serv command | |||
repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) | |||
isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") | |||
//username := os.Getenv(models.EnvRepoUsername) | |||
//reponame := os.Getenv(models.EnvRepoName) | |||
//repoPath := models.RepoPath(username, reponame) | |||
username := os.Getenv(models.EnvRepoUsername) | |||
reponame := os.Getenv(models.EnvRepoName) | |||
userIDStr := os.Getenv(models.EnvPusherID) | |||
repoPath := models.RepoPath(username, reponame) | |||
buf := bytes.NewBuffer(nil) | |||
scanner := bufio.NewScanner(os.Stdin) | |||
@@ -104,36 +105,37 @@ func runHookPreReceive(c *cli.Context) error { | |||
continue | |||
} | |||
//oldCommitID := string(fields[0]) | |||
oldCommitID := string(fields[0]) | |||
newCommitID := string(fields[1]) | |||
refFullName := string(fields[2]) | |||
// FIXME: when we add feature to protected branch to deny force push, then uncomment below | |||
/*var isForce bool | |||
// detect force push | |||
if git.EmptySHA != oldCommitID { | |||
output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath) | |||
if err != nil { | |||
fail("Internal error", "Fail to detect force push: %v", err) | |||
} else if len(output) > 0 { | |||
isForce = true | |||
} | |||
}*/ | |||
branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) | |||
protectBranch, err := private.GetProtectedBranchBy(repoID, branchName) | |||
if err != nil { | |||
log.GitLogger.Fatal(2, "retrieve protected branches information failed") | |||
} | |||
if protectBranch != nil { | |||
if !protectBranch.CanPush { | |||
// check and deletion | |||
if newCommitID == git.EmptySHA { | |||
fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") | |||
} else { | |||
if protectBranch != nil && protectBranch.IsProtected() { | |||
// detect force push | |||
if git.EmptySHA != oldCommitID { | |||
output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).RunInDir(repoPath) | |||
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), "") | |||
} | |||
} | |||
// check and deletion | |||
if newCommitID == git.EmptySHA { | |||
fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") | |||
} else { | |||
userID, _ := strconv.ParseInt(userIDStr, 10, 64) | |||
canPush, err := private.CanUserPush(protectBranch.ID, userID) | |||
if err != nil { | |||
fail("Internal error", "Fail to detect user can push: %v", err) | |||
} else if !canPush { | |||
fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "") | |||
//fail(fmt.Sprintf("branch %s is protected from force push", branchName), "") | |||
} | |||
} | |||
} |
@@ -43,16 +43,15 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { | |||
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") | |||
// Change master branch to protected | |||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches?action=protected_branch", map[string]string{ | |||
"_csrf": csrf, | |||
"branchName": "master", | |||
"canPush": "true", | |||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{ | |||
"_csrf": csrf, | |||
"protected": "on", | |||
}) | |||
resp := session.MakeRequest(t, req, http.StatusOK) | |||
resp := session.MakeRequest(t, req, http.StatusFound) | |||
// Check if master branch has been locked successfully | |||
flashCookie := session.GetCookie("macaron_flash") | |||
assert.NotNil(t, flashCookie) | |||
assert.EqualValues(t, flashCookie.Value, "success%3Dmaster%2BLocked%2Bsuccessfully") | |||
assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bchanged%2Bsuccessfully.", flashCookie.Value) | |||
// Request editor page | |||
req = NewRequest(t, "GET", "/user2/repo1/_new/master/") | |||
@@ -74,6 +73,20 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
// Check body for error message | |||
assert.Contains(t, string(resp.Body), "Can not commit to protected branch 'master'.") | |||
// remove the protected branch | |||
csrf = GetCSRF(t, session, "/user2/repo1/settings/branches") | |||
// Change master branch to protected | |||
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{ | |||
"_csrf": csrf, | |||
"protected": "off", | |||
}) | |||
resp = session.MakeRequest(t, req, http.StatusFound) | |||
// Check if master branch has been locked successfully | |||
flashCookie = session.GetCookie("macaron_flash") | |||
assert.NotNil(t, flashCookie) | |||
assert.EqualValues(t, "success%3DBranch%2Bmaster%2Bprotect%2Boptions%2Bremoved%2Bsuccessfully", flashCookie.Value) | |||
} | |||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath string) *TestResponse { |
@@ -269,7 +269,7 @@ func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestRespo | |||
mac.ServeHTTP(respWriter, req) | |||
if expectedStatus != NoExpectedStatus { | |||
assert.EqualValues(t, expectedStatus, respWriter.HeaderCode, | |||
"Request URL: %s", req.URL.String()) | |||
"Request URL: %s %s", req.URL.String(), buffer.String()) | |||
} | |||
return &TestResponse{ | |||
HeaderCode: respWriter.HeaderCode, |
@@ -31,7 +31,7 @@ func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr, | |||
var branch models.ProtectedBranch | |||
t.Log(string(resp.Body)) | |||
assert.NoError(t, json.Unmarshal(resp.Body, &branch)) | |||
assert.Equal(t, canPush, branch.CanPush) | |||
assert.Equal(t, canPush, !branch.IsProtected()) | |||
} | |||
} | |||
@@ -8,6 +8,12 @@ import ( | |||
"fmt" | |||
"strings" | |||
"time" | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/util" | |||
"github.com/Unknwon/com" | |||
) | |||
const ( | |||
@@ -17,14 +23,43 @@ const ( | |||
// 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 `xorm:"created"` | |||
Updated time.Time `xorm:"-"` | |||
UpdatedUnix int64 `xorm:"updated"` | |||
ID int64 `xorm:"pk autoincr"` | |||
RepoID int64 `xorm:"UNIQUE(s)"` | |||
BranchName string `xorm:"UNIQUE(s)"` | |||
EnableWhitelist bool | |||
WhitelistUserIDs []int64 `xorm:"JSON TEXT"` | |||
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` | |||
Created time.Time `xorm:"-"` | |||
CreatedUnix int64 `xorm:"created"` | |||
Updated time.Time `xorm:"-"` | |||
UpdatedUnix int64 `xorm:"updated"` | |||
} | |||
// IsProtected returns if the branch is protected | |||
func (protectBranch *ProtectedBranch) IsProtected() bool { | |||
return protectBranch.ID > 0 | |||
} | |||
// CanUserPush returns if some user could push to this protected branch | |||
func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool { | |||
if !protectBranch.EnableWhitelist { | |||
return false | |||
} | |||
if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) { | |||
return true | |||
} | |||
if len(protectBranch.WhitelistTeamIDs) == 0 { | |||
return false | |||
} | |||
in, err := IsUserInTeams(userID, protectBranch.WhitelistTeamIDs) | |||
if err != nil { | |||
log.Error(1, "IsUserInTeams:", err) | |||
return false | |||
} | |||
return in | |||
} | |||
// GetProtectedBranchByRepoID getting protected branch by repo ID | |||
@@ -46,6 +81,73 @@ func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, er | |||
return rel, nil | |||
} | |||
// GetProtectedBranchByID getting protected branch by ID | |||
func GetProtectedBranchByID(id int64) (*ProtectedBranch, error) { | |||
rel := &ProtectedBranch{ID: id} | |||
has, err := x.Get(rel) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if !has { | |||
return nil, nil | |||
} | |||
return rel, nil | |||
} | |||
// UpdateProtectBranch saves branch protection options of repository. | |||
// If ID is 0, it creates a new record. Otherwise, updates existing record. | |||
// This function also performs check if whitelist user and team's IDs have been changed | |||
// to avoid unnecessary whitelist delete and regenerate. | |||
func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, whitelistUserIDs, whitelistTeamIDs []int64) (err error) { | |||
if err = repo.GetOwner(); err != nil { | |||
return fmt.Errorf("GetOwner: %v", err) | |||
} | |||
hasUsersChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistUserIDs, whitelistUserIDs) | |||
if hasUsersChanged { | |||
protectBranch.WhitelistUserIDs = make([]int64, 0, len(whitelistUserIDs)) | |||
for _, userID := range whitelistUserIDs { | |||
has, err := hasAccess(x, userID, repo, AccessModeWrite) | |||
if err != nil { | |||
return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err) | |||
} else if !has { | |||
continue // Drop invalid user ID | |||
} | |||
protectBranch.WhitelistUserIDs = append(protectBranch.WhitelistUserIDs, userID) | |||
} | |||
} | |||
// if the repo is in an orgniziation | |||
hasTeamsChanged := !util.IsSliceInt64Eq(protectBranch.WhitelistTeamIDs, whitelistTeamIDs) | |||
if hasTeamsChanged { | |||
teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeWrite) | |||
if err != nil { | |||
return fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) | |||
} | |||
protectBranch.WhitelistTeamIDs = make([]int64, 0, len(teams)) | |||
for i := range teams { | |||
if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(whitelistTeamIDs, teams[i].ID) { | |||
protectBranch.WhitelistTeamIDs = append(protectBranch.WhitelistTeamIDs, teams[i].ID) | |||
} | |||
} | |||
} | |||
// Make sure protectBranch.ID is not 0 for whitelists | |||
if protectBranch.ID == 0 { | |||
if _, err = x.Insert(protectBranch); err != nil { | |||
return fmt.Errorf("Insert: %v", err) | |||
} | |||
return nil | |||
} | |||
if _, err = x.Id(protectBranch.ID).AllCols().Update(protectBranch); err != nil { | |||
return fmt.Errorf("Update: %v", err) | |||
} | |||
return nil | |||
} | |||
// GetProtectedBranches get all protected branches | |||
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) { | |||
protectedBranches := make([]*ProtectedBranch, 0) | |||
@@ -53,7 +155,7 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) { | |||
} | |||
// IsProtectedBranch checks if branch is protected | |||
func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) { | |||
func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) { | |||
protectedBranch := &ProtectedBranch{ | |||
RepoID: repo.ID, | |||
BranchName: branchName, | |||
@@ -63,70 +165,12 @@ func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) { | |||
if err != nil { | |||
return true, err | |||
} else if has { | |||
return true, nil | |||
return !protectedBranch.CanUserPush(doer.ID), nil | |||
} | |||
return false, nil | |||
} | |||
// 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 sess.Close() | |||
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 sess.Close() | |||
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{ | |||
@@ -148,15 +192,3 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { | |||
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 | |||
} |
@@ -128,6 +128,8 @@ var migrations = []Migration{ | |||
NewMigration("remove commits and settings unit types", removeCommitsUnitType), | |||
// v39 -> v40 | |||
NewMigration("adds time tracking and stopwatches", addTimetracking), | |||
// v40 -> v41 | |||
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct), | |||
} | |||
// Migrate database to current version |
@@ -0,0 +1,55 @@ | |||
// Copyright 2017 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 migrations | |||
import ( | |||
"fmt" | |||
"time" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"github.com/go-xorm/xorm" | |||
) | |||
func migrateProtectedBranchStruct(x *xorm.Engine) 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 | |||
} | |||
var pbs []ProtectedBranch | |||
err := x.Find(&pbs) | |||
if err != nil { | |||
return err | |||
} | |||
for _, pb := range pbs { | |||
if pb.CanPush { | |||
if _, err = x.ID(pb.ID).Delete(new(ProtectedBranch)); err != nil { | |||
return err | |||
} | |||
} | |||
} | |||
switch { | |||
case setting.UseSQLite3: | |||
log.Warn("Unable to drop columns in SQLite") | |||
case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB: | |||
if _, err := x.Exec("ALTER TABLE protected_branch DROP COLUMN can_push"); err != nil { | |||
return fmt.Errorf("DROP COLUMN can_push: %v", err) | |||
} | |||
default: | |||
log.Fatal(4, "Unrecognized DB") | |||
} | |||
return nil | |||
} |
@@ -577,6 +577,11 @@ func (org *User) getUserTeamIDs(e Engine, userID int64) ([]int64, error) { | |||
Find(&teamIDs) | |||
} | |||
// TeamsWithAccessToRepo returns all teamsthat have given access level to the repository. | |||
func (org *User) TeamsWithAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) { | |||
return GetTeamsWithAccessToRepo(org.ID, repoID, mode) | |||
} | |||
// GetUserTeamIDs returns of all team IDs of the organization that user is member of. | |||
func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) { | |||
return org.getUserTeamIDs(x, userID) |
@@ -35,6 +35,11 @@ func (t *Team) GetUnitTypes() []UnitType { | |||
return t.UnitTypes | |||
} | |||
// HasWriteAccess returns true if team has at least write level access mode. | |||
func (t *Team) HasWriteAccess() bool { | |||
return t.Authorize >= AccessModeWrite | |||
} | |||
// IsOwnerTeam returns true if team is owner team. | |||
func (t *Team) IsOwnerTeam() bool { | |||
return t.Name == ownerTeamName | |||
@@ -594,6 +599,11 @@ func RemoveTeamMember(team *Team, userID int64) error { | |||
return sess.Commit() | |||
} | |||
// IsUserInTeams returns if a user in some teams | |||
func IsUserInTeams(userID int64, teamIDs []int64) (bool, error) { | |||
return x.Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser)) | |||
} | |||
// ___________ __________ | |||
// \__ ___/___ _____ _____\______ \ ____ ______ ____ | |||
// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \ | |||
@@ -639,3 +649,13 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error { | |||
}) | |||
return err | |||
} | |||
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository. | |||
func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) { | |||
teams := make([]*Team, 0, 5) | |||
return teams, x.Where("team.authorize >= ?", mode). | |||
Join("INNER", "team_repo", "team_repo.team_id = team.id"). | |||
And("team_repo.org_id = ?", orgID). | |||
And("team_repo.repo_id = ?", repoID). | |||
Find(&teams) | |||
} |
@@ -656,6 +656,42 @@ func (repo *Repository) CanEnableEditor() bool { | |||
return !repo.IsMirror | |||
} | |||
// GetWriters returns all users that have write access to the repository. | |||
func (repo *Repository) GetWriters() (_ []*User, err error) { | |||
return repo.getUsersWithAccessMode(x, AccessModeWrite) | |||
} | |||
// getUsersWithAccessMode returns users that have at least given access mode to the repository. | |||
func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []*User, err error) { | |||
if err = repo.getOwner(e); err != nil { | |||
return nil, err | |||
} | |||
accesses := make([]*Access, 0, 10) | |||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil { | |||
return nil, err | |||
} | |||
// Leave a seat for owner itself to append later, but if owner is an organization | |||
// and just waste 1 unit is cheaper than re-allocate memory once. | |||
users := make([]*User, 0, len(accesses)+1) | |||
if len(accesses) > 0 { | |||
userIDs := make([]int64, len(accesses)) | |||
for i := 0; i < len(accesses); i++ { | |||
userIDs[i] = accesses[i].UserID | |||
} | |||
if err = e.In("id", userIDs).Find(&users); err != nil { | |||
return nil, err | |||
} | |||
} | |||
if !repo.Owner.IsOrganization() { | |||
users = append(users, repo.Owner) | |||
} | |||
return users, nil | |||
} | |||
// NextIssueIndex returns the next issue index | |||
// FIXME: should have a mutex to prevent producing same index for two issues that are created | |||
// closely enough. |
@@ -113,6 +113,26 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi | |||
return validate(errs, ctx.Data, f, ctx.Locale) | |||
} | |||
// __________ .__ | |||
// \______ \____________ ____ ____ | |__ | |||
// | | _/\_ __ \__ \ / \_/ ___\| | \ | |||
// | | \ | | \// __ \| | \ \___| Y \ | |||
// |______ / |__| (____ /___| /\___ >___| / | |||
// \/ \/ \/ \/ \/ | |||
// ProtectBranchForm form for changing protected branch settings | |||
type ProtectBranchForm struct { | |||
Protected bool | |||
EnableWhitelist bool | |||
WhitelistUsers string | |||
WhitelistTeams string | |||
} | |||
// Validate validates the fields | |||
func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
return validate(errs, ctx.Data, f, ctx.Locale) | |||
} | |||
// __ __ ___. .__ .__ __ | |||
// / \ / \ ____\_ |__ | |__ | |__ ____ | | __ | |||
// \ \/\/ // __ \| __ \| | \| | \ / _ \| |/ / |
@@ -497,6 +497,16 @@ func Int64sToMap(ints []int64) map[int64]bool { | |||
return m | |||
} | |||
// Int64sContains returns if a int64 in a slice of int64 | |||
func Int64sContains(intsSlice []int64, a int64) bool { | |||
for _, c := range intsSlice { | |||
if c == a { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
// IsLetter reports whether the rune is a letter (category L). | |||
// https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257 | |||
func IsLetter(ch rune) bool { |
@@ -78,8 +78,8 @@ func (r *Repository) CanEnableEditor() bool { | |||
// CanCommitToBranch returns true if repository is editable and user has proper access level | |||
// and branch is not protected | |||
func (r *Repository) CanCommitToBranch() (bool, error) { | |||
protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName) | |||
func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) { | |||
protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName, doer) | |||
if err != nil { | |||
return false, err | |||
} |
@@ -38,3 +38,29 @@ func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBra | |||
return &branch, nil | |||
} | |||
// CanUserPush returns if user can push | |||
func CanUserPush(protectedBranchID, userID int64) (bool, error) { | |||
// Ask for running deliver hook and test pull request tasks. | |||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranch/%d/%d", protectedBranchID, userID) | |||
log.GitLogger.Trace("CanUserPush: %s", reqURL) | |||
resp, err := newInternalRequest(reqURL, "GET").Response() | |||
if err != nil { | |||
return false, err | |||
} | |||
var canPush = make(map[string]interface{}) | |||
if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil { | |||
return false, err | |||
} | |||
defer resp.Body.Close() | |||
// All 2XX status codes are accepted and others will return an error | |||
if resp.StatusCode/100 != 2 { | |||
return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err) | |||
} | |||
return canPush["can_push"].(bool), nil | |||
} |
@@ -0,0 +1,29 @@ | |||
// Copyright 2017 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 util | |||
import "sort" | |||
// Int64Slice attaches the methods of Interface to []int64, sorting in increasing order. | |||
type Int64Slice []int64 | |||
func (p Int64Slice) Len() int { return len(p) } | |||
func (p Int64Slice) Less(i, j int) bool { return p[i] < p[j] } | |||
func (p Int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | |||
// IsSliceInt64Eq returns if the two slice has the same elements but different sequences. | |||
func IsSliceInt64Eq(a, b []int64) bool { | |||
if len(a) != len(b) { | |||
return false | |||
} | |||
sort.Sort(Int64Slice(a)) | |||
sort.Sort(Int64Slice(b)) | |||
for i := 0; i < len(a); i++ { | |||
if a[i] != b[i] { | |||
return false | |||
} | |||
} | |||
return true | |||
} |
@@ -945,11 +945,19 @@ 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.branch_protection = Branch Protection for <b>%s</b> | |||
settings.protect_this_branch = Protect this branch | |||
settings.protect_this_branch_desc = Disable force pushes and prevent deletion. | |||
settings.protect_whitelist_committers = Whitelist who can push to this branch | |||
settings.protect_whitelist_committers_desc = Add users or teams to this branch's whitelist. Whitelisted users bypass the typical push restrictions. | |||
settings.protect_whitelist_users = Users who can push to this branch | |||
settings.protect_whitelist_search_users = Search users | |||
settings.protect_whitelist_teams = Teams whose members can push to this branch. | |||
settings.protect_whitelist_search_teams = Search teams | |||
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.update_protect_branch_success = Branch %s protect options changed successfully. | |||
settings.remove_protected_branch_success= Branch %s protect options removed 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? | |||
settings.default_branch_desc = 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. |
@@ -2344,6 +2344,30 @@ 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.branches .branch-protection .whitelist { | |||
margin-left: 26px; | |||
} | |||
.repository.settings.branches .branch-protection .whitelist .dropdown img { | |||
display: inline-block; | |||
} | |||
.repository.settings.webhook .events .column { | |||
padding-bottom: 0; | |||
} |
@@ -639,42 +639,18 @@ function initRepository() { | |||
if ($('.repository.compare.pull').length > 0) { | |||
initFilterSearchDropdown('.choose.branch .dropdown'); | |||
} | |||
} | |||
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(); | |||
} | |||
// Branches | |||
if ($('.repository.settings.branches').length > 0) { | |||
initFilterSearchDropdown('.protected-branches .dropdown'); | |||
$('.enable-protection, .enable-whitelist').change(function () { | |||
if (this.checked) { | |||
$($(this).data('target')).removeClass('disabled'); | |||
} else { | |||
$($(this).data('target')).addClass('disabled'); | |||
} | |||
); | |||
}); | |||
}); | |||
} | |||
} | |||
function initRepositoryCollaboration() { | |||
@@ -1598,7 +1574,6 @@ $(document).ready(function () { | |||
initEditForm(); | |||
initEditor(); | |||
initOrganization(); | |||
initProtectedBranch(); | |||
initWebhook(); | |||
initAdmin(); | |||
initCodeView(); |
@@ -1251,6 +1251,39 @@ | |||
} | |||
} | |||
&.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; | |||
} | |||
.whitelist { | |||
margin-left: 26px; | |||
.dropdown img { | |||
display: inline-block; | |||
} | |||
} | |||
} | |||
} | |||
&.webhook { | |||
.events { | |||
.column { |
@@ -24,7 +24,29 @@ func GetProtectedBranchBy(ctx *macaron.Context) { | |||
ctx.JSON(200, protectBranch) | |||
} else { | |||
ctx.JSON(200, &models.ProtectedBranch{ | |||
CanPush: true, | |||
ID: 0, | |||
}) | |||
} | |||
} | |||
// CanUserPush returns if user push | |||
func CanUserPush(ctx *macaron.Context) { | |||
pbID := ctx.ParamsInt64(":pbid") | |||
userID := ctx.ParamsInt64(":userid") | |||
protectBranch, err := models.GetProtectedBranchByID(pbID) | |||
if err != nil { | |||
ctx.JSON(500, map[string]interface{}{ | |||
"err": err.Error(), | |||
}) | |||
return | |||
} else if protectBranch != nil { | |||
ctx.JSON(200, map[string]interface{}{ | |||
"can_push": protectBranch.CanUserPush(userID), | |||
}) | |||
} else { | |||
ctx.JSON(200, map[string]interface{}{ | |||
"can_push": false, | |||
}) | |||
} | |||
} |
@@ -42,6 +42,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Group("/", func() { | |||
m.Post("/ssh/:id/update", UpdatePublicKey) | |||
m.Post("/push/update", PushUpdate) | |||
m.Get("/protectedbranch/:pbid/:userid", CanUserPush) | |||
m.Get("/branch/:id/*", GetProtectedBranchBy) | |||
}, CheckInternalToken) | |||
} |
@@ -32,7 +32,7 @@ const ( | |||
) | |||
func renderCommitRights(ctx *context.Context) bool { | |||
canCommit, err := ctx.Repo.CanCommitToBranch() | |||
canCommit, err := ctx.Repo.CanCommitToBranch(ctx.User) | |||
if err != nil { | |||
log.Error(4, "CanCommitToBranch: %v", err) | |||
} |
@@ -694,7 +694,7 @@ func ViewIssue(ctx *context.Context) { | |||
log.Error(4, "GetHeadRepo: %v", err) | |||
} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch && ctx.User.IsWriterOfRepo(pull.HeadRepo) { | |||
// Check if branch is not protected | |||
if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch); err != nil { | |||
if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil { | |||
log.Error(4, "IsProtectedBranch: %v", err) | |||
} else if !protected { | |||
canDelete = true |
@@ -841,7 +841,7 @@ func CleanUpPullRequest(ctx *context.Context) { | |||
} | |||
// Check if branch is not protected | |||
if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch); err != nil || protected { | |||
if protected, err := pr.HeadRepo.IsProtectedBranch(pr.HeadBranch, ctx.User); err != nil || protected { | |||
if err != nil { | |||
log.Error(4, "HeadRepo.IsProtectedBranch: %v", err) | |||
} |
@@ -25,6 +25,7 @@ const ( | |||
tplGithooks base.TplName = "repo/settings/githooks" | |||
tplGithookEdit base.TplName = "repo/settings/githook_edit" | |||
tplDeployKeys base.TplName = "repo/settings/deploy_keys" | |||
tplProtectedBranch base.TplName = "repo/settings/protected_branch" | |||
) | |||
// Settings show a repository's settings page | |||
@@ -437,143 +438,6 @@ 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 := ctx.Query("branch") | |||
if !ctx.Repo.GitRepo.IsBranchExist(branch) { | |||
ctx.Status(404) | |||
return | |||
} else if 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")) |
@@ -0,0 +1,186 @@ | |||
// Copyright 2017 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 repo | |||
import ( | |||
"fmt" | |||
"strings" | |||
"code.gitea.io/git" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/auth" | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
) | |||
// 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 := ctx.Query("branch") | |||
if !ctx.Repo.GitRepo.IsBranchExist(branch) { | |||
ctx.Status(404) | |||
return | |||
} else if 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) | |||
default: | |||
ctx.Handle(404, "", nil) | |||
} | |||
} | |||
// SettingsProtectedBranch renders the protected branch setting page | |||
func SettingsProtectedBranch(c *context.Context) { | |||
branch := c.Params("*") | |||
if !c.Repo.GitRepo.IsBranchExist(branch) { | |||
c.NotFound() | |||
return | |||
} | |||
c.Data["Title"] = c.Tr("repo.settings.protected_branches") + " - " + branch | |||
c.Data["PageIsSettingsBranches"] = true | |||
protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch) | |||
if err != nil { | |||
if !models.IsErrBranchNotExist(err) { | |||
c.Handle(500, "GetProtectBranchOfRepoByName", err) | |||
return | |||
} | |||
} | |||
if protectBranch == nil { | |||
// No options found, create defaults. | |||
protectBranch = &models.ProtectedBranch{ | |||
BranchName: branch, | |||
} | |||
} | |||
users, err := c.Repo.Repository.GetWriters() | |||
if err != nil { | |||
c.Handle(500, "Repo.Repository.GetWriters", err) | |||
return | |||
} | |||
c.Data["Users"] = users | |||
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",") | |||
if c.Repo.Owner.IsOrganization() { | |||
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeWrite) | |||
if err != nil { | |||
c.Handle(500, "Repo.Owner.TeamsWithAccessToRepo", err) | |||
return | |||
} | |||
c.Data["Teams"] = teams | |||
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",") | |||
} | |||
c.Data["Branch"] = protectBranch | |||
c.HTML(200, tplProtectedBranch) | |||
} | |||
// SettingsProtectedBranchPost updates the protected branch settings | |||
func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm) { | |||
branch := ctx.Params("*") | |||
if !ctx.Repo.GitRepo.IsBranchExist(branch) { | |||
ctx.NotFound() | |||
return | |||
} | |||
protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch) | |||
if err != nil { | |||
if !models.IsErrBranchNotExist(err) { | |||
ctx.Handle(500, "GetProtectBranchOfRepoByName", err) | |||
return | |||
} | |||
} | |||
if f.Protected { | |||
if protectBranch == nil { | |||
// No options found, create defaults. | |||
protectBranch = &models.ProtectedBranch{ | |||
RepoID: ctx.Repo.Repository.ID, | |||
BranchName: branch, | |||
} | |||
} | |||
protectBranch.EnableWhitelist = f.EnableWhitelist | |||
whitelistUsers, _ := base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) | |||
whitelistTeams, _ := base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) | |||
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, whitelistUsers, whitelistTeams) | |||
if err != nil { | |||
ctx.Handle(500, "UpdateProtectBranch", err) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch)) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch)) | |||
} else { | |||
if protectBranch != nil { | |||
if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil { | |||
ctx.Handle(500, "DeleteProtectedBranch", err) | |||
return | |||
} | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch)) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
} | |||
} |
@@ -433,8 +433,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
}) | |||
m.Group("/branches", func() { | |||
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) | |||
m.Post("/can_push", repo.ChangeProtectedBranch) | |||
m.Post("/delete", repo.DeleteProtectedBranch) | |||
m.Combo("/*").Get(repo.SettingsProtectedBranch). | |||
Post(bindIgnErr(auth.ProtectBranchForm{}), repo.SettingsProtectedBranchPost) | |||
}, repo.MustBeNotBare) | |||
m.Group("/hooks", func() { |
@@ -39,20 +39,16 @@ | |||
<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" tabindex="0"> | |||
<select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch"> | |||
{{range .LeftBranches}} | |||
<option value="">{{$.i18n.Tr "repo.settings.choose_branch"}}</option> | |||
<option value="{{.}}">{{.}}</option> | |||
{{end}} | |||
</select><i class="dropdown icon"></i> | |||
<i class="dropdown icon"></i> | |||
<div class="default text">{{.i18n.Tr "repo.settings.choose_branch"}}</div> | |||
<div class="menu transition hidden" tabindex="-1" style="display: block !important;"> | |||
{{range .LeftBranches}} | |||
<div class="item" data-value="{{.}}">{{.}}</div> | |||
<a class="item" href="{{$.Repository.Link}}/settings/branches/{{.}}">{{.}}</a> | |||
{{end}} | |||
</div> | |||
</div> | |||
@@ -65,8 +61,8 @@ | |||
<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> | |||
<td><div class="ui basic label blue">{{.BranchName}}</div></td> | |||
<td class="right aligned"><a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/{{.BranchName}}">Edit</a></td> | |||
</tr> | |||
{{else}} | |||
<tr class="center aligned"><td>{{.i18n.Tr "repo.settings.no_protected_branch"}}</td></tr> |
@@ -0,0 +1,74 @@ | |||
{{template "base/head" .}} | |||
<div class="repository settings branches"> | |||
{{template "repo/header" .}} | |||
{{template "repo/settings/navbar" .}} | |||
<div class="ui container"> | |||
{{template "base/alert" .}} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "repo.settings.branch_protection" .Branch.BranchName | Str2html}} | |||
</h4> | |||
<div class="ui attached segment branch-protection"> | |||
<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.IsProtected}}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.IsProtected}}disabled{{end}}"> | |||
<div class="field"> | |||
<div class="ui checkbox"> | |||
<input class="enable-whitelist" name="enable_whitelist" type="checkbox" data-target="#whitelist_box" {{if .Branch.EnableWhitelist}}checked{{end}}> | |||
<label>{{.i18n.Tr "repo.settings.protect_whitelist_committers"}}</label> | |||
<p class="help">{{.i18n.Tr "repo.settings.protect_whitelist_committers_desc"}}</p> | |||
</div> | |||
</div> | |||
<div id="whitelist_box" class="fields {{if not .Branch.EnableWhitelist}}disabled{{end}}"> | |||
<div class="whitelist field"> | |||
<label>{{.i18n.Tr "repo.settings.protect_whitelist_users"}}</label> | |||
<div class="ui multiple search selection dropdown"> | |||
<input type="hidden" name="whitelist_users" value="{{.whitelist_users}}"> | |||
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_users"}}</div> | |||
<div class="menu"> | |||
{{range .Users}} | |||
<div class="item" data-value="{{.ID}}"> | |||
<img class="ui mini image" src="{{.RelAvatarLink}}"> | |||
{{.Name}} | |||
</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> | |||
{{if .Owner.IsOrganization}} | |||
<br> | |||
<div class="whitelist field"> | |||
<label>{{.i18n.Tr "repo.settings.protect_whitelist_teams"}}</label> | |||
<div class="ui multiple search selection dropdown"> | |||
<input type="hidden" name="whitelist_teams" value="{{.whitelist_teams}}"> | |||
<div class="default text">{{.i18n.Tr "repo.settings.protect_whitelist_search_teams"}}</div> | |||
<div class="menu"> | |||
{{range .Teams}} | |||
<div class="item" data-value="{{.ID}}"> | |||
<i class="octicon octicon-jersey"></i> | |||
{{.Name}} | |||
</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> | |||
{{end}} | |||
</div> | |||
</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> | |||
{{template "base/footer" .}} |