improve protected branch to add whitelist support (#2451)
* 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 & typo
This commit is contained in:
		
					parent
					
						
							
								be3319b3d5
							
						
					
				
			
			
				commit
				
					
						1739e84ac0
					
				
			
		
					 29 changed files with 736 additions and 303 deletions
				
			
		
							
								
								
									
										48
									
								
								cmd/hook.go
									
										
									
									
									
								
							
							
						
						
									
										48
									
								
								cmd/hook.go
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										55
									
								
								models/migrations/v40.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								models/migrations/v40.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								modules/util/compare.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								modules/util/compare.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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();
 | 
			
		||||
                }
 | 
			
		||||
    // 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');
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('.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() {
 | 
			
		||||
| 
						 | 
				
			
			@ -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"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										186
									
								
								routers/repo/setting_protected_branch.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								routers/repo/setting_protected_branch.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										74
									
								
								templates/repo/settings/protected_branch.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								templates/repo/settings/protected_branch.tmpl
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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" .}}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue