Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protected branches with templates #14334

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions models/branches.go
Original file line number Diff line number Diff line change
@@ -294,66 +294,75 @@ type WhitelistOptions struct {
// 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, opts WhitelistOptions) (err error) {
if err = repo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err)
return updateProtectBranch(x, repo, protectBranch, opts)
}

func updateProtectBranch(e Engine, repo *Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
if err = repo.getOwner(e); err != nil {
return fmt.Errorf("getOwner: %v", err)
}

whitelist, err := updateUserWhitelist(repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
whitelist, err := updateUserWhitelist(e, repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil {
return err
}
protectBranch.WhitelistUserIDs = whitelist

whitelist, err = updateUserWhitelist(repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
whitelist, err = updateUserWhitelist(e, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistUserIDs = whitelist

whitelist, err = updateApprovalWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
whitelist, err = updateApprovalWhitelist(e, repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist

// if the repo is in an organization
whitelist, err = updateTeamWhitelist(repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
whitelist, err = updateTeamWhitelist(e, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
return err
}
protectBranch.WhitelistTeamIDs = whitelist

whitelist, err = updateTeamWhitelist(repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
whitelist, err = updateTeamWhitelist(e, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistTeamIDs = whitelist

whitelist, err = updateTeamWhitelist(repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
whitelist, err = updateTeamWhitelist(e, repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist

// Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 {
if _, err = x.Insert(protectBranch); err != nil {
if _, err = e.Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %v", err)
}
return nil
}

if _, err = x.ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
if _, err = e.ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %v", err)
}

return nil
}

// GetProtectedBranches get all protected branches
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
func (repo *Repository) getProtectedBranches(e Engine) ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0)
return protectedBranches, x.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
return protectedBranches, e.Find(&protectedBranches, &ProtectedBranch{RepoID: repo.ID})
}

// GetProtectedBranches get all protected branches
func (repo *Repository) GetProtectedBranches() ([]*ProtectedBranch, error) {
return repo.getProtectedBranches(x)
}

// GetBranchProtection get the branch protection of a branch
@@ -402,15 +411,15 @@ func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User)

// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
func updateApprovalWhitelist(e Engine, repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}

whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if reader, err := repo.IsReader(userID); err != nil {
if reader, err := repo.isReader(e, userID); err != nil {
return nil, err
} else if !reader {
continue
@@ -423,19 +432,19 @@ func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []

// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have write access to the repo.
func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
func updateUserWhitelist(e Engine, repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}

whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
user, err := GetUserByID(userID)
user, err := getUserByID(e, userID)
if err != nil {
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
}
perm, err := GetUserRepoPermission(repo, user)
perm, err := getUserRepoPermission(e, repo, user)
if err != nil {
return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
}
@@ -452,13 +461,13 @@ func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int6

// updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
// the teams from newWhitelist which have write access to the repo.
func updateTeamWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
func updateTeamWhitelist(e Engine, repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasTeamsChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist)
if !hasTeamsChanged {
return currentWhitelist, nil
}

teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, AccessModeRead)
teams, err := getTeamsWithAccessToRepo(e, repo.OwnerID, repo.ID, AccessModeRead)
if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
}
6 changes: 5 additions & 1 deletion models/org_team.go
Original file line number Diff line number Diff line change
@@ -1019,8 +1019,12 @@ func removeTeamRepo(e Engine, teamID, repoID int64) error {

// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
return getTeamsWithAccessToRepo(x, orgID, repoID, mode)
}

func getTeamsWithAccessToRepo(e Engine, orgID, repoID int64, mode AccessMode) ([]*Team, error) {
teams := make([]*Team, 0, 5)
return teams, x.Where("team.authorize >= ?", mode).
return teams, e.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).
6 changes: 5 additions & 1 deletion models/repo.go
Original file line number Diff line number Diff line change
@@ -824,10 +824,14 @@ func (repo *Repository) GetWriters() (_ []*User, err error) {

// IsReader returns true if user has explicit read access or higher to the repository.
func (repo *Repository) IsReader(userID int64) (bool, error) {
return repo.isReader(x, userID)
}

func (repo *Repository) isReader(e Engine, userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
return x.Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, AccessModeRead).Get(&Access{})
return e.Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, AccessModeRead).Get(&Access{})
}

// getUsersWithAccessMode returns users that have at least given access mode to the repository.
66 changes: 57 additions & 9 deletions models/repo_generate.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
package models

import (
"fmt"
"strconv"
"strings"

@@ -18,15 +19,16 @@ import (

// GenerateRepoOptions contains the template units to generate
type GenerateRepoOptions struct {
Name string
Description string
Private bool
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
IssueLabels bool
Name string
Description string
Private bool
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
IssueLabels bool
BranchProtection bool
}

// IsValid checks whether at least one option is chosen for generation
@@ -166,3 +168,49 @@ func GenerateIssueLabels(ctx DBContext, templateRepo, generateRepo *Repository)
}
return nil
}

// GenerateBranchProtection generates branch protection from a template repository
func GenerateBranchProtection(ctx DBContext, doer *User, templateRepo, generateRepo *Repository) error {
branches, err := templateRepo.getProtectedBranches(ctx.e)
if err != nil {
return err
}

for _, branch := range branches {
// Create the branches (other than default, which exists already)
if !strings.EqualFold(generateRepo.DefaultBranch, branch.BranchName) {
if err := git.Push(generateRepo.RepoPath(), git.PushOptions{
Remote: generateRepo.RepoPath(),
Branch: fmt.Sprintf("%s:%s%s", generateRepo.DefaultBranch, git.BranchPrefix, branch.BranchName),
Env: InternalPushingEnvironment(doer, generateRepo),
}); err != nil {
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
return fmt.Errorf("push: %v", err)
}
}

// Copy protections
protectBranch := &ProtectedBranch{
RepoID: generateRepo.ID,
BranchName: branch.BranchName,
CanPush: branch.CanPush,
EnableStatusCheck: branch.EnableStatusCheck,
StatusCheckContexts: branch.StatusCheckContexts,
RequiredApprovals: branch.RequiredApprovals,
BlockOnRejectedReviews: branch.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: branch.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: branch.BlockOnOutdatedBranch,
DismissStaleApprovals: branch.DismissStaleApprovals,
RequireSignedCommits: branch.RequireSignedCommits,
ProtectedFilePatterns: branch.ProtectedFilePatterns,
}

if err := updateProtectBranch(ctx.e, generateRepo, protectBranch, WhitelistOptions{}); err != nil {
return err
}
}

return nil
}
1 change: 1 addition & 0 deletions modules/repository/generate.go
Original file line number Diff line number Diff line change
@@ -252,6 +252,7 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template
IsFsckEnabled: templateRepo.IsFsckEnabled,
TemplateID: templateRepo.ID,
TrustModel: templateRepo.TrustModel,
DefaultBranch: templateRepo.DefaultBranch,
}

if err = models.CreateRepository(ctx, doer, owner, generateRepo, false); err != nil {
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
@@ -771,6 +771,7 @@ template.webhooks = Webhooks
template.topics = Topics
template.avatar = Avatar
template.issue_labels = Issue Labels
template.branch_protection = Branch Protection
template.one_item = Must select at least one template item
template.invalid = Must select a template repository

19 changes: 10 additions & 9 deletions routers/repo/repo.go
Original file line number Diff line number Diff line change
@@ -211,15 +211,16 @@ func CreatePost(ctx *context.Context) {
var err error
if form.RepoTemplate > 0 {
opts := models.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private,
GitContent: form.GitContent,
Topics: form.Topics,
GitHooks: form.GitHooks,
Webhooks: form.Webhooks,
Avatar: form.Avatar,
IssueLabels: form.Labels,
Name: form.RepoName,
Description: form.Description,
Private: form.Private,
GitContent: form.GitContent,
Topics: form.Topics,
GitHooks: form.GitHooks,
Webhooks: form.Webhooks,
Avatar: form.Avatar,
IssueLabels: form.Labels,
BranchProtection: form.BranchProtection,
}

if !opts.IsValid() {
18 changes: 10 additions & 8 deletions services/forms/repo_form.go
Original file line number Diff line number Diff line change
@@ -41,14 +41,16 @@ type CreateRepoForm struct {
Readme string
Template bool

RepoTemplate int64
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
Labels bool
TrustModel string
RepoTemplate int64
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
Labels bool
BranchProtection bool

TrustModel string
}

// Validate validates the fields
7 changes: 7 additions & 0 deletions services/repository/generate.go
Original file line number Diff line number Diff line change
@@ -25,6 +25,13 @@ func GenerateRepository(doer, owner *models.User, templateRepo *models.Repositor
if err = repo_module.GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
return err
}

// Branch Protection
if opts.BranchProtection {
if err := models.GenerateBranchProtection(ctx, doer, templateRepo, generateRepo); err != nil {
return err
}
}
}

// Topics
9 changes: 8 additions & 1 deletion templates/repo/create.tmpl
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@
<div class="inline field">
<label>{{.i18n.Tr "repo.template.items"}}</label>
<div class="ui checkbox">
<input class="hidden" name="git_content" type="checkbox" tabindex="0" {{if .git_content}}checked{{end}}>
<input class="hidden" id="git_content" name="git_content" type="checkbox" tabindex="0" {{if .git_content}}checked{{end}}>
<label>{{.i18n.Tr "repo.template.git_content"}}</label>
</div>
<div class="ui checkbox{{if not .SignedUser.CanEditGitHook}} poping up{{end}}"{{if not .SignedUser.CanEditGitHook}} data-content="{{.i18n.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
@@ -108,6 +108,13 @@
<label>{{.i18n.Tr "repo.template.issue_labels"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input class="hidden" id="branch_protection" name="branch_protection" type="checkbox" tabindex="0" {{if .branch_protection}}checked{{end}} {{if not .git_content}}disabled{{end}}>
<label>{{.i18n.Tr "repo.template.branch_protection"}}</label>
</div>
</div>
</div>

<div id="non_template">
10 changes: 10 additions & 0 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
@@ -2450,6 +2450,15 @@ function initWipTitle() {
});
}

function initTemplateBranchProtection() {
const $gitContent = $('#git_content');
const $branchProtection = $('#branch_protection');
$gitContent.on('change', () => {
$branchProtection.prop('checked', false);
$branchProtection.prop('disabled', !$gitContent.is(':checked'));
});
}

function initTemplateSearch() {
const $repoTemplate = $('#repo_template');
const checkTemplate = function () {
@@ -2797,6 +2806,7 @@ $(document).ready(async () => {
initWipTitle();
initPullRequestReview();
initRepoStatusChecker();
initTemplateBranchProtection();
initTemplateSearch();
initIssueReferenceRepositorySearch();
initContextPopups();