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
				
			
		
							
								
								
									
										40
									
								
								cmd/hook.go
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								cmd/hook.go
									
									
									
									
									
								
							|  | @ -84,9 +84,10 @@ func runHookPreReceive(c *cli.Context) error { | ||||||
| 	// the environment setted on serv command
 | 	// the environment setted on serv command
 | ||||||
| 	repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) | 	repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) | ||||||
| 	isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") | 	isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") | ||||||
| 	//username := os.Getenv(models.EnvRepoUsername)
 | 	username := os.Getenv(models.EnvRepoUsername) | ||||||
| 	//reponame := os.Getenv(models.EnvRepoName)
 | 	reponame := os.Getenv(models.EnvRepoName) | ||||||
| 	//repoPath := models.RepoPath(username, reponame)
 | 	userIDStr := os.Getenv(models.EnvPusherID) | ||||||
|  | 	repoPath := models.RepoPath(username, reponame) | ||||||
| 
 | 
 | ||||||
| 	buf := bytes.NewBuffer(nil) | 	buf := bytes.NewBuffer(nil) | ||||||
| 	scanner := bufio.NewScanner(os.Stdin) | 	scanner := bufio.NewScanner(os.Stdin) | ||||||
|  | @ -104,36 +105,37 @@ func runHookPreReceive(c *cli.Context) error { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		//oldCommitID := string(fields[0])
 | 		oldCommitID := string(fields[0]) | ||||||
| 		newCommitID := string(fields[1]) | 		newCommitID := string(fields[1]) | ||||||
| 		refFullName := string(fields[2]) | 		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) | 		branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) | ||||||
| 		protectBranch, err := private.GetProtectedBranchBy(repoID, branchName) | 		protectBranch, err := private.GetProtectedBranchBy(repoID, branchName) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.GitLogger.Fatal(2, "retrieve protected branches information failed") | 			log.GitLogger.Fatal(2, "retrieve protected branches information failed") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if protectBranch != nil { | 		if protectBranch != nil && protectBranch.IsProtected() { | ||||||
| 			if !protectBranch.CanPush { | 			// 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
 | 			// check and deletion
 | ||||||
| 			if newCommitID == git.EmptySHA { | 			if newCommitID == git.EmptySHA { | ||||||
| 				fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") | 				fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") | ||||||
| 			} else { | 			} 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("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") | 	csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") | ||||||
| 	// Change master branch to protected
 | 	// Change master branch to protected
 | ||||||
| 	req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches?action=protected_branch", map[string]string{ | 	req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{ | ||||||
| 		"_csrf":     csrf, | 		"_csrf":     csrf, | ||||||
| 		"branchName": "master", | 		"protected": "on", | ||||||
| 		"canPush":    "true", |  | ||||||
| 	}) | 	}) | ||||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | 	resp := session.MakeRequest(t, req, http.StatusFound) | ||||||
| 	// Check if master branch has been locked successfully
 | 	// Check if master branch has been locked successfully
 | ||||||
| 	flashCookie := session.GetCookie("macaron_flash") | 	flashCookie := session.GetCookie("macaron_flash") | ||||||
| 	assert.NotNil(t, flashCookie) | 	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
 | 	// Request editor page
 | ||||||
| 	req = NewRequest(t, "GET", "/user2/repo1/_new/master/") | 	req = NewRequest(t, "GET", "/user2/repo1/_new/master/") | ||||||
|  | @ -74,6 +73,20 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	// Check body for error message
 | 	// Check body for error message
 | ||||||
| 	assert.Contains(t, string(resp.Body), "Can not commit to protected branch 'master'.") | 	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 { | 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) | 	mac.ServeHTTP(respWriter, req) | ||||||
| 	if expectedStatus != NoExpectedStatus { | 	if expectedStatus != NoExpectedStatus { | ||||||
| 		assert.EqualValues(t, expectedStatus, respWriter.HeaderCode, | 		assert.EqualValues(t, expectedStatus, respWriter.HeaderCode, | ||||||
| 			"Request URL: %s", req.URL.String()) | 			"Request URL: %s %s", req.URL.String(), buffer.String()) | ||||||
| 	} | 	} | ||||||
| 	return &TestResponse{ | 	return &TestResponse{ | ||||||
| 		HeaderCode: respWriter.HeaderCode, | 		HeaderCode: respWriter.HeaderCode, | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr, | ||||||
| 		var branch models.ProtectedBranch | 		var branch models.ProtectedBranch | ||||||
| 		t.Log(string(resp.Body)) | 		t.Log(string(resp.Body)) | ||||||
| 		assert.NoError(t, json.Unmarshal(resp.Body, &branch)) | 		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" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 
 | ||||||
|  | 	"github.com/Unknwon/com" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -20,13 +26,42 @@ type ProtectedBranch struct { | ||||||
| 	ID               int64  `xorm:"pk autoincr"` | 	ID               int64  `xorm:"pk autoincr"` | ||||||
| 	RepoID           int64  `xorm:"UNIQUE(s)"` | 	RepoID           int64  `xorm:"UNIQUE(s)"` | ||||||
| 	BranchName       string `xorm:"UNIQUE(s)"` | 	BranchName       string `xorm:"UNIQUE(s)"` | ||||||
| 	CanPush     bool | 	EnableWhitelist  bool | ||||||
|  | 	WhitelistUserIDs []int64   `xorm:"JSON TEXT"` | ||||||
|  | 	WhitelistTeamIDs []int64   `xorm:"JSON TEXT"` | ||||||
| 	Created          time.Time `xorm:"-"` | 	Created          time.Time `xorm:"-"` | ||||||
| 	CreatedUnix      int64     `xorm:"created"` | 	CreatedUnix      int64     `xorm:"created"` | ||||||
| 	Updated          time.Time `xorm:"-"` | 	Updated          time.Time `xorm:"-"` | ||||||
| 	UpdatedUnix      int64     `xorm:"updated"` | 	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
 | // GetProtectedBranchByRepoID getting protected branch by repo ID
 | ||||||
| func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) { | func GetProtectedBranchByRepoID(RepoID int64) ([]*ProtectedBranch, error) { | ||||||
| 	protectedBranches := make([]*ProtectedBranch, 0) | 	protectedBranches := make([]*ProtectedBranch, 0) | ||||||
|  | @ -46,6 +81,73 @@ func GetProtectedBranchBy(repoID int64, BranchName string) (*ProtectedBranch, er | ||||||
| 	return rel, nil | 	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
 | // GetProtectedBranches get all protected branches
 | ||||||
| func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) { | func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) { | ||||||
| 	protectedBranches := make([]*ProtectedBranch, 0) | 	protectedBranches := make([]*ProtectedBranch, 0) | ||||||
|  | @ -53,7 +155,7 @@ func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IsProtectedBranch checks if branch is protected
 | // 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{ | 	protectedBranch := &ProtectedBranch{ | ||||||
| 		RepoID:     repo.ID, | 		RepoID:     repo.ID, | ||||||
| 		BranchName: branchName, | 		BranchName: branchName, | ||||||
|  | @ -63,70 +165,12 @@ func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return true, err | 		return true, err | ||||||
| 	} else if has { | 	} else if has { | ||||||
| 		return true, nil | 		return !protectedBranch.CanUserPush(doer.ID), nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return false, 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.
 | // DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
 | ||||||
| func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { | func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { | ||||||
| 	protectedBranch := &ProtectedBranch{ | 	protectedBranch := &ProtectedBranch{ | ||||||
|  | @ -148,15 +192,3 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { | ||||||
| 
 | 
 | ||||||
| 	return sess.Commit() | 	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), | 	NewMigration("remove commits and settings unit types", removeCommitsUnitType), | ||||||
| 	// v39 -> v40
 | 	// v39 -> v40
 | ||||||
| 	NewMigration("adds time tracking and stopwatches", addTimetracking), | 	NewMigration("adds time tracking and stopwatches", addTimetracking), | ||||||
|  | 	// v40 -> v41
 | ||||||
|  | 	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Migrate database to current version
 | // 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) | 		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.
 | // GetUserTeamIDs returns of all team IDs of the organization that user is member of.
 | ||||||
| func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) { | func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) { | ||||||
| 	return org.getUserTeamIDs(x, userID) | 	return org.getUserTeamIDs(x, userID) | ||||||
|  |  | ||||||
|  | @ -35,6 +35,11 @@ func (t *Team) GetUnitTypes() []UnitType { | ||||||
| 	return t.UnitTypes | 	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.
 | // IsOwnerTeam returns true if team is owner team.
 | ||||||
| func (t *Team) IsOwnerTeam() bool { | func (t *Team) IsOwnerTeam() bool { | ||||||
| 	return t.Name == ownerTeamName | 	return t.Name == ownerTeamName | ||||||
|  | @ -594,6 +599,11 @@ func RemoveTeamMember(team *Team, userID int64) error { | ||||||
| 	return sess.Commit() | 	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 | 	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 | 	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
 | // NextIssueIndex returns the next issue index
 | ||||||
| // FIXME: should have a mutex to prevent producing same index for two issues that are created
 | // FIXME: should have a mutex to prevent producing same index for two issues that are created
 | ||||||
| // closely enough.
 | // 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) | 	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 | 	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).
 | // IsLetter reports whether the rune is a letter (category L).
 | ||||||
| // https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257
 | // https://github.com/golang/go/blob/master/src/go/scanner/scanner.go#L257
 | ||||||
| func IsLetter(ch rune) bool { | 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
 | // CanCommitToBranch returns true if repository is editable and user has proper access level
 | ||||||
| //   and branch is not protected
 | //   and branch is not protected
 | ||||||
| func (r *Repository) CanCommitToBranch() (bool, error) { | func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) { | ||||||
| 	protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName) | 	protectedBranch, err := r.Repository.IsProtectedBranch(r.BranchName, doer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -38,3 +38,29 @@ func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBra | ||||||
| 
 | 
 | ||||||
| 	return &branch, nil | 	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=Allow push? | ||||||
| settings.protected_branch_can_push_yes=You can push | settings.protected_branch_can_push_yes=You can push | ||||||
| settings.protected_branch_can_push_no=You can not 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.add_protected_branch=Enable protection | ||||||
| settings.delete_protected_branch=Disable protection | settings.delete_protected_branch=Disable protection | ||||||
| settings.add_protected_branch_success=%s Locked successfully | settings.update_protect_branch_success = Branch %s protect options changed successfully. | ||||||
| settings.add_protected_branch_failed= %s Locked failed | settings.remove_protected_branch_success= Branch %s protect options removed successfully | ||||||
| settings.remove_protected_branch_success=%s Unlocked successfully |  | ||||||
| settings.protected_branch_deletion=To delete a protected branch | 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.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. | 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-left: 5px; | ||||||
|   margin-top: -3px; |   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 { | .repository.settings.webhook .events .column { | ||||||
|   padding-bottom: 0; |   padding-bottom: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -639,42 +639,18 @@ function initRepository() { | ||||||
|     if ($('.repository.compare.pull').length > 0) { |     if ($('.repository.compare.pull').length > 0) { | ||||||
|         initFilterSearchDropdown('.choose.branch .dropdown'); |         initFilterSearchDropdown('.choose.branch .dropdown'); | ||||||
|     } |     } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function initProtectedBranch() { |     // Branches
 | ||||||
|     $('#protectedBranch').change(function () { |     if ($('.repository.settings.branches').length > 0) { | ||||||
|         var $this = $(this); |         initFilterSearchDropdown('.protected-branches .dropdown'); | ||||||
|         $.post($this.data('url'), { |         $('.enable-protection, .enable-whitelist').change(function () { | ||||||
|                 "_csrf": csrf, |             if (this.checked) { | ||||||
|                 "canPush": true, |                 $($(this).data('target')).removeClass('disabled'); | ||||||
|                 "branchName": $this.val(), |  | ||||||
|             }, |  | ||||||
|             function (data) { |  | ||||||
|                 if (data.redirect) { |  | ||||||
|                     window.location.href = data.redirect; |  | ||||||
|             } else { |             } else { | ||||||
|                     location.reload(); |                 $($(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() { | function initRepositoryCollaboration() { | ||||||
|  | @ -1598,7 +1574,6 @@ $(document).ready(function () { | ||||||
|     initEditForm(); |     initEditForm(); | ||||||
|     initEditor(); |     initEditor(); | ||||||
|     initOrganization(); |     initOrganization(); | ||||||
|     initProtectedBranch(); |  | ||||||
|     initWebhook(); |     initWebhook(); | ||||||
|     initAdmin(); |     initAdmin(); | ||||||
|     initCodeView(); |     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 { | 		&.webhook { | ||||||
| 			.events { | 			.events { | ||||||
| 				.column { | 				.column { | ||||||
|  |  | ||||||
|  | @ -24,7 +24,29 @@ func GetProtectedBranchBy(ctx *macaron.Context) { | ||||||
| 		ctx.JSON(200, protectBranch) | 		ctx.JSON(200, protectBranch) | ||||||
| 	} else { | 	} else { | ||||||
| 		ctx.JSON(200, &models.ProtectedBranch{ | 		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.Group("/", func() { | ||||||
| 		m.Post("/ssh/:id/update", UpdatePublicKey) | 		m.Post("/ssh/:id/update", UpdatePublicKey) | ||||||
| 		m.Post("/push/update", PushUpdate) | 		m.Post("/push/update", PushUpdate) | ||||||
|  | 		m.Get("/protectedbranch/:pbid/:userid", CanUserPush) | ||||||
| 		m.Get("/branch/:id/*", GetProtectedBranchBy) | 		m.Get("/branch/:id/*", GetProtectedBranchBy) | ||||||
| 	}, CheckInternalToken) | 	}, CheckInternalToken) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ const ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func renderCommitRights(ctx *context.Context) bool { | func renderCommitRights(ctx *context.Context) bool { | ||||||
| 	canCommit, err := ctx.Repo.CanCommitToBranch() | 	canCommit, err := ctx.Repo.CanCommitToBranch(ctx.User) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error(4, "CanCommitToBranch: %v", err) | 		log.Error(4, "CanCommitToBranch: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -694,7 +694,7 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 				log.Error(4, "GetHeadRepo: %v", err) | 				log.Error(4, "GetHeadRepo: %v", err) | ||||||
| 			} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch && ctx.User.IsWriterOfRepo(pull.HeadRepo) { | 			} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch && ctx.User.IsWriterOfRepo(pull.HeadRepo) { | ||||||
| 				// Check if branch is not protected
 | 				// 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) | 					log.Error(4, "IsProtectedBranch: %v", err) | ||||||
| 				} else if !protected { | 				} else if !protected { | ||||||
| 					canDelete = true | 					canDelete = true | ||||||
|  |  | ||||||
|  | @ -841,7 +841,7 @@ func CleanUpPullRequest(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check if branch is not protected
 | 	// 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 { | 		if err != nil { | ||||||
| 			log.Error(4, "HeadRepo.IsProtectedBranch: %v", err) | 			log.Error(4, "HeadRepo.IsProtectedBranch: %v", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ const ( | ||||||
| 	tplGithooks        base.TplName = "repo/settings/githooks" | 	tplGithooks        base.TplName = "repo/settings/githooks" | ||||||
| 	tplGithookEdit     base.TplName = "repo/settings/githook_edit" | 	tplGithookEdit     base.TplName = "repo/settings/githook_edit" | ||||||
| 	tplDeployKeys      base.TplName = "repo/settings/deploy_keys" | 	tplDeployKeys      base.TplName = "repo/settings/deploy_keys" | ||||||
|  | 	tplProtectedBranch base.TplName = "repo/settings/protected_branch" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Settings show a repository's settings page
 | // 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
 | // parseOwnerAndRepo get repos by owner
 | ||||||
| func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | ||||||
| 	owner, err := models.GetUserByName(ctx.Params(":username")) | 	owner, err := models.GetUserByName(ctx.Params(":username")) | ||||||
|  |  | ||||||
							
								
								
									
										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.Group("/branches", func() { | ||||||
| 				m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) | 				m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) | ||||||
| 				m.Post("/can_push", repo.ChangeProtectedBranch) | 				m.Combo("/*").Get(repo.SettingsProtectedBranch). | ||||||
| 				m.Post("/delete", repo.DeleteProtectedBranch) | 					Post(bindIgnErr(auth.ProtectBranchForm{}), repo.SettingsProtectedBranchPost) | ||||||
| 			}, repo.MustBeNotBare) | 			}, repo.MustBeNotBare) | ||||||
| 
 | 
 | ||||||
| 			m.Group("/hooks", func() { | 			m.Group("/hooks", func() { | ||||||
|  |  | ||||||
|  | @ -39,20 +39,16 @@ | ||||||
| 		<h4 class="ui top attached header"> | 		<h4 class="ui top attached header"> | ||||||
| 			{{.i18n.Tr "repo.settings.protected_branch"}} | 			{{.i18n.Tr "repo.settings.protected_branch"}} | ||||||
| 		</h4> | 		</h4> | ||||||
|  | 
 | ||||||
| 		<div class="ui attached table segment"> | 		<div class="ui attached table segment"> | ||||||
| 			<div class="ui grid padded"> | 			<div class="ui grid padded"> | ||||||
| 				<div class="eight wide column"> | 				<div class="eight wide column"> | ||||||
| 					<div class="ui fluid dropdown selection" tabindex="0"> | 					<div class="ui fluid dropdown selection" tabindex="0"> | ||||||
| 						<select id="protectedBranch" name="branch" data-url="{{.Repository.Link}}/settings/branches?action=protected_branch"> | 						<i class="dropdown icon"></i> | ||||||
| 							{{range .LeftBranches}} |  | ||||||
| 								<option value="">{{$.i18n.Tr "repo.settings.choose_branch"}}</option> |  | ||||||
| 								<option value="{{.}}">{{.}}</option> |  | ||||||
| 							{{end}} |  | ||||||
| 						</select><i class="dropdown icon"></i> |  | ||||||
| 						<div class="default text">{{.i18n.Tr "repo.settings.choose_branch"}}</div> | 						<div class="default text">{{.i18n.Tr "repo.settings.choose_branch"}}</div> | ||||||
| 						<div class="menu transition hidden" tabindex="-1" style="display: block !important;"> | 						<div class="menu transition hidden" tabindex="-1" style="display: block !important;"> | ||||||
| 							{{range .LeftBranches}} | 							{{range .LeftBranches}} | ||||||
| 								<div class="item" data-value="{{.}}">{{.}}</div> | 								<a class="item" href="{{$.Repository.Link}}/settings/branches/{{.}}">{{.}}</a> | ||||||
| 							{{end}} | 							{{end}} | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  | @ -65,8 +61,8 @@ | ||||||
| 						<tbody> | 						<tbody> | ||||||
| 							{{range .ProtectedBranches}} | 							{{range .ProtectedBranches}} | ||||||
| 								<tr> | 								<tr> | ||||||
| 									<td><div class="ui large label">{{.BranchName}}</div></td> | 									<td><div class="ui basic label blue">{{.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 class="right aligned"><a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/{{.BranchName}}">Edit</a></td> | ||||||
| 								</tr> | 								</tr> | ||||||
| 							{{else}} | 							{{else}} | ||||||
| 								<tr class="center aligned"><td>{{.i18n.Tr "repo.settings.no_protected_branch"}}</td></tr> | 								<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…
	
		Reference in a new issue