From b5c7eaa054b4d403636ef37f67a5178363863288 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 13 Jan 2021 22:33:33 -0600 Subject: [PATCH 1/3] Protected branches with templates Signed-off-by: jolheiser --- models/branches.go | 43 ++++++++++++--------- models/org_team.go | 6 ++- models/repo.go | 6 ++- models/repo_generate.go | 68 ++++++++++++++++++++++++++++----- modules/auth/repo_form.go | 18 +++++---- modules/repository/generate.go | 1 + options/locale/locale_en-US.ini | 1 + routers/repo/repo.go | 19 ++++----- services/repository/generate.go | 7 ++++ templates/repo/create.tmpl | 9 ++++- web_src/js/index.js | 10 +++++ 11 files changed, 141 insertions(+), 47 deletions(-) diff --git a/models/branches.go b/models/branches.go index 80df8c433eb0f..aa179c566ac4d 100644 --- a/models/branches.go +++ b/models/branches.go @@ -311,42 +311,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) { + return updateProtectBranch(x, repo, protectBranch, opts) +} + +func updateProtectBranch(e Engine, repo *Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) { if err = repo.GetOwner(); 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 } @@ -354,13 +358,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) } @@ -368,9 +372,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 @@ -419,7 +428,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 @@ -427,7 +436,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 @@ -440,7 +449,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 @@ -448,11 +457,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) } @@ -469,13 +478,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 a3f1eb92a2af3..54881a097e17c 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -1021,8 +1021,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 f2453cc5c73a9..898be2adc1eb3 100644 --- a/models/repo.go +++ b/models/repo.go @@ -821,10 +821,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..2e39432c7db4b 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -5,6 +5,7 @@ package models import ( + "fmt" "strconv" "strings" @@ -18,20 +19,21 @@ 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 func (gro GenerateRepoOptions) IsValid() bool { - return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added + return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels || gro.BranchProtection // or other items as they are added } // GiteaTemplate holds information about a .gitea/template file @@ -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/auth/repo_form.go b/modules/auth/repo_form.go index 78b2197a2dda8..597392fd19238 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/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/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 5f21c75f76d28..0342a1196b85e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -731,6 +731,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 2614389aaaca8..d8fc1bec6a1e0 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -205,15 +205,16 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { 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/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 7d71043c34b59..b2058bc122a88 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -70,7 +70,7 @@
- +
@@ -100,6 +100,13 @@
+
+ +
+ + +
+
diff --git a/web_src/js/index.js b/web_src/js/index.js index 07d5b99e31167..5551b49eccd9d 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2291,6 +2291,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 () { @@ -2552,6 +2561,7 @@ $(document).ready(async () => { initWipTitle(); initPullRequestReview(); initRepoStatusChecker(); + initTemplateBranchProtection(); initTemplateSearch(); initContextPopups(); initTableSort(); From 28a0f10ccffbd9f7557694abe2292566e9ed2a10 Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Thu, 14 Jan 2021 11:11:40 -0600 Subject: [PATCH 2/3] Update models/repo_generate.go --- models/repo_generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo_generate.go b/models/repo_generate.go index 2e39432c7db4b..a9d6b7af5ac54 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -33,7 +33,7 @@ type GenerateRepoOptions struct { // IsValid checks whether at least one option is chosen for generation func (gro GenerateRepoOptions) IsValid() bool { - return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels || gro.BranchProtection // or other items as they are added + return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added } // GiteaTemplate holds information about a .gitea/template file From 8db06f0a5525532fc6dd0b93bf8d35a7877a283f Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 13 Apr 2021 22:48:58 -0500 Subject: [PATCH 3/3] Send engine to getOwner Signed-off-by: jolheiser --- models/branches.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/branches.go b/models/branches.go index a9c309707c99b..6348644095ecf 100644 --- a/models/branches.go +++ b/models/branches.go @@ -298,8 +298,8 @@ func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts } func updateProtectBranch(e Engine, repo *Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) { - if err = repo.GetOwner(); err != nil { - return fmt.Errorf("GetOwner: %v", err) + if err = repo.getOwner(e); err != nil { + return fmt.Errorf("getOwner: %v", err) } whitelist, err := updateUserWhitelist(e, repo, protectBranch.WhitelistUserIDs, opts.UserIDs)