Browse Source

repo: add protect branch whitelist (#4177)

Add options to add users and teams to whitelist of a protected
branch. This is only available for organizational repositories.
Unknwon 7 years ago
parent
commit
6072e9a52c

+ 6 - 0
cmd/hook.go

@@ -100,6 +100,12 @@ func runHookPreReceive(c *cli.Context) error {
 			continue
 		}
 
+		// Check if whitelist is enabled
+		userID := com.StrTo(os.Getenv(http.ENV_AUTH_USER_ID)).MustInt64()
+		if protectBranch.EnableWhitelist && !models.IsUserInProtectBranchWhitelist(repoID, userID, branchName) {
+			fail(fmt.Sprintf("Branch '%s' is protected and you are not in the push whitelist", branchName), "")
+		}
+
 		// Check if branch allows direct push
 		if protectBranch.RequirePullRequest {
 			fail(fmt.Sprintf("Branch '%s' is protected and commits must be merged through pull request", branchName), "")

+ 4 - 0
conf/locale/locale_en-US.ini

@@ -656,6 +656,10 @@ settings.protect_require_pull_request = Require pull request instead direct push
 settings.protect_require_pull_request_desc = Enable this option to disable direct pushing to this branch. Commits have to be pushed to another non-protected branch and merged to this branch through pull request.
 settings.protect_whitelist_committers = Whitelist who can push to this branch
 settings.protect_whitelist_committers_desc = Add people or teams to whitelist of direct push to this branch.
+settings.protect_whitelist_users = Users who can push to this branch
+settings.protect_whitelist_search_users = Search users
+settings.protect_whitelist_teams = Teams for which members of them can push to this branch
+settings.protect_whitelist_search_teams = Search teams
 settings.update_protect_branch_success = Protect options for this branch has been updated successfully!
 settings.hooks = Webhooks
 settings.githooks = Git Hooks

+ 1 - 1
gogs.go

@@ -16,7 +16,7 @@ import (
 	"github.com/gogits/gogs/modules/setting"
 )
 
-const APP_VER = "0.9.167.0223 / 0.10 RC"
+const APP_VER = "0.9.168.0223 / 0.10 RC"
 
 func init() {
 	setting.AppVer = APP_VER

+ 1 - 1
models/models.go

@@ -66,7 +66,7 @@ func init() {
 		new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
 		new(Label), new(IssueLabel), new(Milestone),
 		new(Mirror), new(Release), new(LoginSource), new(Webhook), new(HookTask),
-		new(ProtectBranch),
+		new(ProtectBranch), new(ProtectBranchWhitelist),
 		new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
 		new(Notice), new(EmailAddress))
 

+ 8 - 7
models/org.go

@@ -32,10 +32,10 @@ func (org *User) IsOrgMember(uid int64) bool {
 }
 
 func (org *User) getTeam(e Engine, name string) (*Team, error) {
-	return getTeam(e, org.ID, name)
+	return getTeamOfOrgByName(e, org.ID, name)
 }
 
-// GetTeam returns named team of organization.
+// GetTeamOfOrgByName returns named team of organization.
 func (org *User) GetTeam(name string) (*Team, error) {
 	return org.getTeam(x, name)
 }
@@ -49,8 +49,9 @@ func (org *User) GetOwnerTeam() (*Team, error) {
 	return org.getOwnerTeam(x)
 }
 
-func (org *User) getTeams(e Engine) error {
-	return e.Where("org_id=?", org.ID).Find(&org.Teams)
+func (org *User) getTeams(e Engine) (err error) {
+	org.Teams, err = getTeamsByOrgID(e, org.ID)
+	return err
 }
 
 // GetTeams returns all teams that belong to organization.
@@ -502,7 +503,7 @@ func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repos
 	repos := make([]*Repository, 0, pageSize)
 	// FIXME: use XORM chain operations instead of raw SQL.
 	if err = x.Sql(fmt.Sprintf(`SELECT repository.* FROM repository
-	INNER JOIN team_repo 
+	INNER JOIN team_repo
 	ON team_repo.repo_id = repository.id
 	WHERE (repository.owner_id = ? AND repository.is_private = ?) OR team_repo.team_id IN (%s)
 	GROUP BY repository.id
@@ -514,7 +515,7 @@ func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repos
 	}
 
 	results, err := x.Query(fmt.Sprintf(`SELECT repository.id FROM repository
-	INNER JOIN team_repo 
+	INNER JOIN team_repo
 	ON team_repo.repo_id = repository.id
 	WHERE (repository.owner_id = ? AND repository.is_private = ?) OR team_repo.team_id IN (%s)
 	GROUP BY repository.id
@@ -541,7 +542,7 @@ func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error)
 
 	repos := make([]*Repository, 0, 10)
 	if err = x.Sql(fmt.Sprintf(`SELECT repository.* FROM repository
-	INNER JOIN team_repo 
+	INNER JOIN team_repo
 	ON team_repo.repo_id = repository.id AND repository.is_mirror = ?
 	WHERE (repository.owner_id = ? AND repository.is_private = ?) OR team_repo.team_id IN (%s)
 	GROUP BY repository.id

+ 26 - 11
models/org_team.go

@@ -43,9 +43,14 @@ func (t *Team) IsOwnerTeam() bool {
 	return t.Name == OWNER_TEAM
 }
 
+// HasWriteAccess returns true if team has at least write level access mode.
+func (t *Team) HasWriteAccess() bool {
+	return t.Authorize >= ACCESS_MODE_WRITE
+}
+
 // IsTeamMember returns true if given user is a member of team.
-func (t *Team) IsMember(uid int64) bool {
-	return IsTeamMember(t.OrgID, t.ID, uid)
+func (t *Team) IsMember(userID int64) bool {
+	return IsTeamMember(t.OrgID, t.ID, userID)
 }
 
 func (t *Team) getRepositories(e Engine) (err error) {
@@ -260,9 +265,9 @@ func NewTeam(t *Team) error {
 	return sess.Commit()
 }
 
-func getTeam(e Engine, orgId int64, name string) (*Team, error) {
+func getTeamOfOrgByName(e Engine, orgID int64, name string) (*Team, error) {
 	t := &Team{
-		OrgID:     orgId,
+		OrgID:     orgID,
 		LowerName: strings.ToLower(name),
 	}
 	has, err := e.Get(t)
@@ -274,14 +279,14 @@ func getTeam(e Engine, orgId int64, name string) (*Team, error) {
 	return t, nil
 }
 
-// GetTeam returns team by given team name and organization.
-func GetTeam(orgId int64, name string) (*Team, error) {
-	return getTeam(x, orgId, name)
+// GetTeamOfOrgByName returns team by given team name and organization.
+func GetTeamOfOrgByName(orgID int64, name string) (*Team, error) {
+	return getTeamOfOrgByName(x, orgID, name)
 }
 
-func getTeamByID(e Engine, teamId int64) (*Team, error) {
+func getTeamByID(e Engine, teamID int64) (*Team, error) {
 	t := new(Team)
-	has, err := e.Id(teamId).Get(t)
+	has, err := e.Id(teamID).Get(t)
 	if err != nil {
 		return nil, err
 	} else if !has {
@@ -291,8 +296,18 @@ func getTeamByID(e Engine, teamId int64) (*Team, error) {
 }
 
 // GetTeamByID returns team by given ID.
-func GetTeamByID(teamId int64) (*Team, error) {
-	return getTeamByID(x, teamId)
+func GetTeamByID(teamID int64) (*Team, error) {
+	return getTeamByID(x, teamID)
+}
+
+func getTeamsByOrgID(e Engine, orgID int64) ([]*Team, error) {
+	teams := make([]*Team, 0, 3)
+	return teams, e.Where("org_id = ?", orgID).Find(&teams)
+}
+
+// GetTeamsByOrgID returns all teams belong to given organization.
+func GetTeamsByOrgID(orgID int64) ([]*Team, error) {
+	return getTeamsByOrgID(x, orgID)
 }
 
 // UpdateTeam updates information of team.

+ 16 - 6
models/repo.go

@@ -329,14 +329,14 @@ func (repo *Repository) DeleteWiki() {
 	}
 }
 
-// getAssignees returns a list of users who can be assigned to issues in this repository.
-func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
+// getUsersWithAccesMode returns users that have at least given access mode to the repository.
+func (repo *Repository) getUsersWithAccesMode(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, ACCESS_MODE_READ).Find(&accesses); err != nil {
+	if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
 		return nil, err
 	}
 
@@ -360,7 +360,12 @@ func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
 	return users, nil
 }
 
-// GetAssignees returns all users that have write access and can be assigned to issues
+// getAssignees returns a list of users who can be assigned to issues in this repository.
+func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
+	return repo.getUsersWithAccesMode(e, ACCESS_MODE_READ)
+}
+
+// GetAssignees returns all users that have read access and can be assigned to issues
 // of the repository,
 func (repo *Repository) GetAssignees() (_ []*User, err error) {
 	return repo.getAssignees(x)
@@ -371,6 +376,11 @@ func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
 	return GetAssigneeByID(repo, userID)
 }
 
+// GetWriters returns all users that have write access to the repository.
+func (repo *Repository) GetWriters() (_ []*User, err error) {
+	return repo.getUsersWithAccesMode(x, ACCESS_MODE_WRITE)
+}
+
 // GetMilestoneByID returns the milestone belongs to repository by given ID.
 func (repo *Repository) GetMilestoneByID(milestoneID int64) (*Milestone, error) {
 	return GetMilestoneByRepoID(repo.ID, milestoneID)
@@ -1015,10 +1025,10 @@ func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error
 		}
 
 		_, stderr, err := process.ExecDir(-1,
-			repoPath, fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath),
+			repoPath, fmt.Sprintf("CreateRepository 'git update-server-info': %s", repoPath),
 			"git", "update-server-info")
 		if err != nil {
-			return nil, errors.New("CreateRepository(git update-server-info): " + stderr)
+			return nil, errors.New("CreateRepository 'git update-server-info': " + stderr)
 		}
 	}
 

+ 142 - 3
models/repo_branch.go

@@ -6,8 +6,12 @@ package models
 
 import (
 	"fmt"
+	"strings"
 
+	"github.com/Unknwon/com"
 	"github.com/gogits/git-module"
+
+	"github.com/gogits/gogs/modules/base"
 )
 
 type Branch struct {
@@ -58,6 +62,20 @@ func (br *Branch) GetCommit() (*git.Commit, error) {
 	return gitRepo.GetBranchCommit(br.Name)
 }
 
+type ProtectBranchWhitelist struct {
+	ID              int64
+	ProtectBranchID int64
+	RepoID          int64  `xorm:"UNIQUE(protect_branch_whitelist)"`
+	Name            string `xorm:"UNIQUE(protect_branch_whitelist)"`
+	UserID          int64  `xorm:"UNIQUE(protect_branch_whitelist)"`
+}
+
+// IsUserInProtectBranchWhitelist returns true if given user is in the whitelist of a branch in a repository.
+func IsUserInProtectBranchWhitelist(repoID, userID int64, branch string) bool {
+	has, err := x.Where("repo_id = ?", repoID).And("user_id = ?", userID).And("name = ?", branch).Get(new(ProtectBranchWhitelist))
+	return has && err == nil
+}
+
 // ProtectBranch contains options of a protected branch.
 type ProtectBranch struct {
 	ID                 int64
@@ -65,6 +83,9 @@ type ProtectBranch struct {
 	Name               string `xorm:"UNIQUE(protect_branch)"`
 	Protected          bool
 	RequirePullRequest bool
+	EnableWhitelist    bool
+	WhitelistUserIDs   string `xorm:"TEXT"`
+	WhitelistTeamIDs   string `xorm:"TEXT"`
 }
 
 // GetProtectBranchOfRepoByName returns *ProtectBranch by branch name in given repostiory.
@@ -94,15 +115,133 @@ func IsBranchOfRepoRequirePullRequest(repoID int64, name string) bool {
 // UpdateProtectBranch saves branch protection options.
 // If ID is 0, it creates a new record. Otherwise, updates existing record.
 func UpdateProtectBranch(protectBranch *ProtectBranch) (err error) {
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if protectBranch.ID == 0 {
+		if _, err = sess.Insert(protectBranch); err != nil {
+			return fmt.Errorf("Insert: %v", err)
+		}
+		return
+	}
+
+	if _, err = sess.Id(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
+		return fmt.Errorf("Update: %v", err)
+	}
+
+	return sess.Commit()
+}
+
+// UpdateOrgProtectBranch saves branch protection options of organizational 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 UpdateOrgProtectBranch(repo *Repository, protectBranch *ProtectBranch, whitelistUserIDs, whitelistTeamIDs string) (err error) {
+	if err = repo.GetOwner(); err != nil {
+		return fmt.Errorf("GetOwner: %v", err)
+	} else if !repo.Owner.IsOrganization() {
+		return fmt.Errorf("expect repository owner to be an organization")
+	}
+
+	hasUsersChanged := false
+	validUserIDs := base.StringsToInt64s(strings.Split(protectBranch.WhitelistUserIDs, ","))
+	if protectBranch.WhitelistUserIDs != whitelistUserIDs {
+		hasUsersChanged = true
+		userIDs := base.StringsToInt64s(strings.Split(whitelistUserIDs, ","))
+		validUserIDs = make([]int64, 0, len(userIDs))
+		for _, userID := range userIDs {
+			has, err := HasAccess(userID, repo, ACCESS_MODE_WRITE)
+			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
+			}
+
+			validUserIDs = append(validUserIDs, userID)
+		}
+
+		protectBranch.WhitelistUserIDs = strings.Join(base.Int64sToStrings(validUserIDs), ",")
+	}
+
+	hasTeamsChanged := false
+	validTeamIDs := base.StringsToInt64s(strings.Split(protectBranch.WhitelistTeamIDs, ","))
+	if protectBranch.WhitelistTeamIDs != whitelistTeamIDs {
+		hasTeamsChanged = true
+		teamIDs := base.StringsToInt64s(strings.Split(whitelistTeamIDs, ","))
+		teams, err := GetTeamsByOrgID(repo.OwnerID)
+		if err != nil {
+			return fmt.Errorf("GetTeamsByOrgID [org_id: %d]: %v", repo.OwnerID, err)
+		}
+		validTeamIDs = make([]int64, 0, len(teams))
+		for i := range teams {
+			if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(teamIDs, teams[i].ID) {
+				validTeamIDs = append(validTeamIDs, teams[i].ID)
+			}
+		}
+
+		protectBranch.WhitelistTeamIDs = strings.Join(base.Int64sToStrings(validTeamIDs), ",")
+	}
+
+	// Merge users and members of teams
+	var whitelists []*ProtectBranchWhitelist
+	if hasUsersChanged || hasTeamsChanged {
+		mergedUserIDs := make(map[int64]bool)
+		for _, userID := range validUserIDs {
+			mergedUserIDs[userID] = true
+		}
+
+		for _, teamID := range validTeamIDs {
+			members, err := GetTeamMembers(teamID)
+			if err != nil {
+				return fmt.Errorf("GetTeamMembers [team_id: %d]: %v", teamID, err)
+			}
+
+			for i := range members {
+				mergedUserIDs[members[i].ID] = true
+			}
+		}
+
+		whitelists = make([]*ProtectBranchWhitelist, 0, len(mergedUserIDs))
+		for userID := range mergedUserIDs {
+			whitelists = append(whitelists, &ProtectBranchWhitelist{
+				ProtectBranchID: protectBranch.ID,
+				RepoID:          repo.ID,
+				Name:            protectBranch.Name,
+				UserID:          userID,
+			})
+		}
+	}
+
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
 	if protectBranch.ID == 0 {
-		if _, err = x.Insert(protectBranch); err != nil {
+		if _, err = sess.Insert(protectBranch); err != nil {
 			return fmt.Errorf("Insert: %v", err)
 		}
 		return
 	}
 
-	_, err = x.Id(protectBranch.ID).AllCols().Update(protectBranch)
-	return err
+	if _, err = sess.Id(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
+		return fmt.Errorf("Update: %v", err)
+	}
+
+	// Refresh whitelists
+	if hasUsersChanged || hasTeamsChanged {
+		if _, err = sess.Delete(&ProtectBranchWhitelist{ProtectBranchID: protectBranch.ID}); err != nil {
+			return fmt.Errorf("delete old protect branch whitelists: %v", err)
+		} else if _, err = sess.Insert(whitelists); err != nil {
+			return fmt.Errorf("insert new protect branch whitelists: %v", err)
+		}
+	}
+
+	return sess.Commit()
 }
 
 // GetProtectBranchesByRepoID returns a list of *ProtectBranch in given repostiory.

+ 3 - 0
modules/auth/repo_form.go

@@ -116,6 +116,9 @@ func (f *RepoSettingForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
 type ProtectBranchForm struct {
 	Protected          bool
 	RequirePullRequest bool
+	EnableWhitelist    bool
+	WhitelistUsers     string
+	WhitelistTeams     string
 }
 
 func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

File diff suppressed because it is too large
+ 1 - 1
modules/bindata/bindata.go


+ 6 - 0
public/css/gogs.css

@@ -2310,6 +2310,12 @@ footer .ui.language .menu {
   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.webhooks .types .menu .item {
   padding: 10px !important;
 }

+ 4 - 2
public/js/gogs.js

@@ -341,7 +341,7 @@ function initRepository() {
     // Branches
     if ($('.repository.settings.branches').length > 0) {
         initFilterSearchDropdown('.protected-branches .dropdown');
-        $('.enable-protection').change(function () {
+        $('.enable-protection, .enable-whitelist').change(function () {
             if (this.checked) {
                 $($(this).data('target')).removeClass('disabled');
             } else {
@@ -1223,7 +1223,9 @@ $(document).ready(function () {
     });
 
     // Semantic UI modules.
-    $('.dropdown').dropdown();
+    $('.ui.dropdown').dropdown({
+        forceSelection: false
+    });
     $('.jump.dropdown').dropdown({
         action: 'hide',
         onShow: function () {

+ 7 - 0
public/less/_repository.less

@@ -1330,6 +1330,13 @@
 					margin-left: 20px;
 					display: block;
 				}
+				.whitelist {
+					margin-left: 26px;
+
+					.dropdown img {
+						display: inline-block;
+					}
+				}
 			}
 		}
 

+ 25 - 3
routers/repo/setting.go

@@ -415,7 +415,6 @@ func SettingsProtectedBranch(ctx *context.Context) {
 
 	ctx.Data["Title"] = ctx.Tr("repo.settings.protected_branches") + " - " + branch
 	ctx.Data["PageIsSettingsBranches"] = true
-	ctx.Data["IsOrgRepo"] = ctx.Repo.Owner.IsOrganization()
 
 	protectBranch, err := models.GetProtectBranchOfRepoByName(ctx.Repo.Repository.ID, branch)
 	if err != nil {
@@ -430,6 +429,23 @@ func SettingsProtectedBranch(ctx *context.Context) {
 		}
 	}
 
+	if ctx.Repo.Owner.IsOrganization() {
+		users, err := ctx.Repo.Repository.GetWriters()
+		if err != nil {
+			ctx.Handle(500, "Repo.Repository.GetPushers", err)
+			return
+		}
+		ctx.Data["Users"] = users
+		ctx.Data["whitelist_users"] = protectBranch.WhitelistUserIDs
+
+		if err = ctx.Repo.Owner.GetTeams(); err != nil {
+			ctx.Handle(500, "Repo.Owner.GetTeams", err)
+			return
+		}
+		ctx.Data["Teams"] = ctx.Repo.Owner.Teams
+		ctx.Data["whitelist_teams"] = protectBranch.WhitelistTeamIDs
+	}
+
 	ctx.Data["Branch"] = protectBranch
 	ctx.HTML(200, SETTINGS_PROTECTED_BRANCH)
 }
@@ -457,8 +473,14 @@ func SettingsProtectedBranchPost(ctx *context.Context, form auth.ProtectBranchFo
 
 	protectBranch.Protected = form.Protected
 	protectBranch.RequirePullRequest = form.RequirePullRequest
-	if err = models.UpdateProtectBranch(protectBranch); err != nil {
-		ctx.Handle(500, "UpdateProtectBranch", err)
+	protectBranch.EnableWhitelist = form.EnableWhitelist
+	if ctx.Repo.Owner.IsOrganization() {
+		err = models.UpdateOrgProtectBranch(ctx.Repo.Repository, protectBranch, form.WhitelistUsers, form.WhitelistTeams)
+	} else {
+		err = models.UpdateProtectBranch(protectBranch)
+	}
+	if err != nil {
+		ctx.Handle(500, "UpdateOrgProtectBranch/UpdateProtectBranch", err)
 		return
 	}
 

+ 1 - 1
templates/.VERSION

@@ -1 +1 @@
-0.9.167.0223 / 0.10 RC
+0.9.168.0223 / 0.10 RC

+ 37 - 2
templates/repo/settings/protected_branch.tmpl

@@ -28,14 +28,49 @@
 									<p class="help">{{.i18n.Tr "repo.settings.protect_require_pull_request_desc"}}</p>
 								</div>
 							</div>
-							{{if .IsOrgRepo}}
+							{{if .Owner.IsOrganization}}
 								<div class="field">
 									<div class="ui checkbox">
-										<input name="whitelist_committers" type="checkbox" data-target="#whitelist_box">
+										<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="field {{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>
+									<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}}
+													{{if and (not .IsOwnerTeam) .HasWriteAccess}}
+														<div class="item" data-value="{{.ID}}">
+															<i class="octicon octicon-jersey"></i>
+															{{.Name}}
+														</div>
+													{{end}}
+												{{end}}
+											</div>
+										</div>
+									</div>
+								</div>
 							{{end}}
 						</div>
 

Some files were not shown because too many files changed in this diff