diff --git a/models/branches.go b/models/branches.go index 1ac1fa49e5809..6348644095ecf 100644 --- a/models/branches.go +++ b/models/branches.go @@ -294,42 +294,46 @@ 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 } @@ -337,13 +341,13 @@ func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts // 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) } @@ -351,9 +355,14 @@ func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts } // 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,7 +411,7 @@ 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 @@ -410,7 +419,7 @@ func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist [] 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,7 +432,7 @@ 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 @@ -431,11 +440,11 @@ func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int6 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) } diff --git a/models/org_team.go b/models/org_team.go index 6226772b9a828..03a80f337eed8 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -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). diff --git a/models/repo.go b/models/repo.go index bdb84ee00da55..19dc28d6e6932 100644 --- a/models/repo.go +++ b/models/repo.go @@ -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. diff --git a/models/repo_generate.go b/models/repo_generate.go index b0016494c459e..a9d6b7af5ac54 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -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 +} diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 1ba457fb3a551..171a68085bbcc 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -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 { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c2f835a98c0d2..966172230cf7b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/routers/repo/repo.go b/routers/repo/repo.go index b066fd315c02e..5521ff9f931de 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -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() { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 55d1f6e3bc386..f965fc9d47236 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 diff --git a/services/repository/generate.go b/services/repository/generate.go index 067f8f61d0a06..584faacceb3e5 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -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 diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 4aabcb6bb377b..7a22ffb160bde 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -78,7 +78,7 @@
- +
@@ -108,6 +108,13 @@
+
+ +
+ + +
+
diff --git a/web_src/js/index.js b/web_src/js/index.js index 1716df9e7fa64..e54c43db836e3 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -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();