mirror of
https://github.com/go-gitea/gitea.git
synced 2024-11-27 12:39:29 +08:00
Allow force push to protected branches (#28086)
Fixes #22722 ### Problem Currently, it is not possible to force push to a branch with branch protection rules in place. There are often times where this is necessary (CI workflows/administrative tasks etc). The current workaround is to rename/remove the branch protection, perform the force push, and then reinstate the protections. ### Solution Provide an additional section in the branch protection rules to allow users to specify which users with push access can also force push to the branch. The default value of the rule will be set to `Disabled`, and the UI is intuitive and very similar to the `Push` section. It is worth noting in this implementation that allowing force push does not override regular push access, and both will need to be enabled for a user to force push. This applies to manual force push to a remote, and also in Gitea UI updating a PR by rebase (which requires force push) This modifies the `BranchProtection` API structs to add: - `enable_force_push bool` - `enable_force_push_whitelist bool` - `force_push_whitelist_usernames string[]` - `force_push_whitelist_teams string[]` - `force_push_whitelist_deploy_keys bool` ### Updated Branch Protection UI: <img width="943" alt="image" src="https://github.com/go-gitea/gitea/assets/79623665/7491899c-d816-45d5-be84-8512abd156bf"> ### Pull Request `Update branch by Rebase` option enabled with source branch `test` being a protected branch: ![image](https://github.com/go-gitea/gitea/assets/79623665/e018e6e9-b7b2-4bd3-808e-4947d7da35cc) <img width="1038" alt="image" src="https://github.com/go-gitea/gitea/assets/79623665/57ead13e-9006-459f-b83c-7079e6f4c654"> --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
9c00dda33a
commit
12cb1d2998
@ -44,6 +44,11 @@ type ProtectedBranch struct {
|
||||
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
|
||||
StatusCheckContexts []string `xorm:"JSON TEXT"`
|
||||
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
@ -143,6 +148,33 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
|
||||
return in
|
||||
}
|
||||
|
||||
// CanUserForcePush returns if some user could force push to this protected branch
|
||||
// Since force-push extends normal push, we also check if user has regular push access
|
||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
|
||||
if !protectBranch.CanForcePush {
|
||||
return false
|
||||
}
|
||||
|
||||
if !protectBranch.EnableForcePushAllowlist {
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
||||
return protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ForcePushAllowlistTeamIDs)
|
||||
if err != nil {
|
||||
log.Error("IsUserInTeams: %v", err)
|
||||
return false
|
||||
}
|
||||
return in && protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
|
||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
|
||||
if !protectBranch.EnableMergeWhitelist {
|
||||
@ -301,6 +333,9 @@ type WhitelistOptions struct {
|
||||
UserIDs []int64
|
||||
TeamIDs []int64
|
||||
|
||||
ForcePushUserIDs []int64
|
||||
ForcePushTeamIDs []int64
|
||||
|
||||
MergeUserIDs []int64
|
||||
MergeTeamIDs []int64
|
||||
|
||||
@ -328,6 +363,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
}
|
||||
protectBranch.WhitelistUserIDs = whitelist
|
||||
|
||||
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.ForcePushAllowlistUserIDs, opts.ForcePushUserIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protectBranch.ForcePushAllowlistUserIDs = whitelist
|
||||
|
||||
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -347,6 +388,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
}
|
||||
protectBranch.WhitelistTeamIDs = whitelist
|
||||
|
||||
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ForcePushAllowlistTeamIDs, opts.ForcePushTeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protectBranch.ForcePushAllowlistTeamIDs = whitelist
|
||||
|
||||
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -468,43 +515,58 @@ func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserIDFromProtectedBranch remove all user ids from protected branch options
|
||||
// removeIDsFromProtectedBranch is a helper function to remove IDs from protected branch options
|
||||
func removeIDsFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID, teamID int64, columnNames []string) error {
|
||||
lenUserIDs, lenForcePushIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ForcePushAllowlistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
|
||||
lenTeamIDs, lenForcePushTeamIDs, lenApprovalTeamIDs, lenMergeTeamIDs := len(p.WhitelistTeamIDs), len(p.ForcePushAllowlistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
|
||||
|
||||
if userID > 0 {
|
||||
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
|
||||
p.ForcePushAllowlistUserIDs = util.SliceRemoveAll(p.ForcePushAllowlistUserIDs, userID)
|
||||
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
|
||||
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
|
||||
}
|
||||
|
||||
if teamID > 0 {
|
||||
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
|
||||
p.ForcePushAllowlistTeamIDs = util.SliceRemoveAll(p.ForcePushAllowlistTeamIDs, teamID)
|
||||
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
|
||||
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
|
||||
}
|
||||
|
||||
if (lenUserIDs != len(p.WhitelistUserIDs) ||
|
||||
lenForcePushIDs != len(p.ForcePushAllowlistUserIDs) ||
|
||||
lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistUserIDs)) ||
|
||||
(lenTeamIDs != len(p.WhitelistTeamIDs) ||
|
||||
lenForcePushTeamIDs != len(p.ForcePushAllowlistTeamIDs) ||
|
||||
lenApprovalTeamIDs != len(p.ApprovalsWhitelistTeamIDs) ||
|
||||
lenMergeTeamIDs != len(p.MergeWhitelistTeamIDs)) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(columnNames...).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserIDFromProtectedBranch removes all user ids from protected branch options
|
||||
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
|
||||
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
|
||||
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
|
||||
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
|
||||
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
|
||||
|
||||
if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistUserIDs) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||
"whitelist_user_i_ds",
|
||||
"merge_whitelist_user_i_ds",
|
||||
"approvals_whitelist_user_i_ds",
|
||||
).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
columnNames := []string{
|
||||
"whitelist_user_i_ds",
|
||||
"force_push_allowlist_user_i_ds",
|
||||
"merge_whitelist_user_i_ds",
|
||||
"approvals_whitelist_user_i_ds",
|
||||
}
|
||||
return nil
|
||||
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
|
||||
}
|
||||
|
||||
// RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options
|
||||
// RemoveTeamIDFromProtectedBranch removes all team ids from protected branch options
|
||||
func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error {
|
||||
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
|
||||
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
|
||||
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
|
||||
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
|
||||
|
||||
if lenIDs != len(p.WhitelistTeamIDs) ||
|
||||
lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) ||
|
||||
lenMergeIDs != len(p.MergeWhitelistTeamIDs) {
|
||||
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||
"whitelist_team_i_ds",
|
||||
"merge_whitelist_team_i_ds",
|
||||
"approvals_whitelist_team_i_ds",
|
||||
).Update(p); err != nil {
|
||||
return fmt.Errorf("updateProtectedBranches: %v", err)
|
||||
}
|
||||
columnNames := []string{
|
||||
"whitelist_team_i_ds",
|
||||
"force_push_allowlist_team_i_ds",
|
||||
"merge_whitelist_team_i_ds",
|
||||
"approvals_whitelist_team_i_ds",
|
||||
}
|
||||
return nil
|
||||
return removeIDsFromProtectedBranch(ctx, p, 0, teamID, columnNames)
|
||||
}
|
||||
|
@ -591,6 +591,8 @@ var migrations = []Migration{
|
||||
|
||||
// v299 -> v300
|
||||
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
|
||||
// v300 -> v301
|
||||
NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
17
models/migrations/v1_23/v300.go
Normal file
17
models/migrations/v1_23/v300.go
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddForcePushBranchProtection(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
}
|
@ -30,6 +30,11 @@ type BranchProtection struct {
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
|
||||
EnableForcePush bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
@ -63,6 +68,11 @@ type CreateBranchProtectionOption struct {
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"`
|
||||
EnableForcePush bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistDeployKeys bool `json:"force_push_allowlist_deploy_keys"`
|
||||
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
@ -89,6 +99,11 @@ type EditBranchProtectionOption struct {
|
||||
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
|
||||
PushWhitelistTeams []string `json:"push_whitelist_teams"`
|
||||
PushWhitelistDeployKeys *bool `json:"push_whitelist_deploy_keys"`
|
||||
EnableForcePush *bool `json:"enable_force_push"`
|
||||
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
|
||||
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
|
||||
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
|
||||
ForcePushAllowlistDeployKeys *bool `json:"force_push_allowlist_deploy_keys"`
|
||||
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
|
||||
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
|
||||
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
|
||||
|
@ -2280,6 +2280,7 @@ settings.event_wiki_desc = Wiki page created, renamed, edited or deleted.
|
||||
settings.event_release = Release
|
||||
settings.event_release_desc = Release published, updated or deleted in a repository.
|
||||
settings.event_push = Push
|
||||
settings.event_force_push = Force Push
|
||||
settings.event_push_desc = Git push to a repository.
|
||||
settings.event_repository = Repository
|
||||
settings.event_repository_desc = Repository created or deleted.
|
||||
@ -2373,19 +2374,28 @@ settings.protect_this_branch = Enable Branch Protection
|
||||
settings.protect_this_branch_desc = Prevents deletion and restricts Git pushing and merging to the branch.
|
||||
settings.protect_disable_push = Disable Push
|
||||
settings.protect_disable_push_desc = No pushing will be allowed to this branch.
|
||||
settings.protect_disable_force_push = Disable Force Push
|
||||
settings.protect_disable_force_push_desc = No force pushing will be allowed to this branch.
|
||||
settings.protect_enable_push = Enable Push
|
||||
settings.protect_enable_push_desc = Anyone with write access will be allowed to push to this branch (but not force push).
|
||||
settings.protect_enable_force_push_all = Enable Force Push
|
||||
settings.protect_enable_force_push_all_desc = Anyone with push access will be allowed to force push to this branch.
|
||||
settings.protect_enable_force_push_allowlist = Allowlist Restricted Force Push
|
||||
settings.protect_enable_force_push_allowlist_desc = Only allowlisted users or teams with push access will be allowed to force push to this branch.
|
||||
settings.protect_enable_merge = Enable Merge
|
||||
settings.protect_enable_merge_desc = Anyone with write access will be allowed to merge the pull requests into this branch.
|
||||
settings.protect_whitelist_committers = Whitelist Restricted Push
|
||||
settings.protect_whitelist_committers_desc = Only whitelisted users or teams will be allowed to push to this branch (but not force push).
|
||||
settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push.
|
||||
settings.protect_whitelist_users = Whitelisted users for pushing:
|
||||
settings.protect_whitelist_teams = Whitelisted teams for pushing:
|
||||
settings.protect_merge_whitelist_committers = Enable Merge Whitelist
|
||||
settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch.
|
||||
settings.protect_merge_whitelist_users = Whitelisted users for merging:
|
||||
settings.protect_merge_whitelist_teams = Whitelisted teams for merging:
|
||||
settings.protect_whitelist_committers = Allowlist Restricted Push
|
||||
settings.protect_whitelist_committers_desc = Only allowlisted users or teams will be allowed to push to this branch (but not force push).
|
||||
settings.protect_whitelist_deploy_keys = Allowlist deploy keys with write access to push.
|
||||
settings.protect_whitelist_users = Allowlisted users for pushing:
|
||||
settings.protect_whitelist_teams = Allowlisted teams for pushing:
|
||||
settings.protect_force_push_allowlist_users = Allowlisted users for force pushing:
|
||||
settings.protect_force_push_allowlist_teams = Allowlisted teams for force pushing:
|
||||
settings.protect_force_push_allowlist_deploy_keys = Allowlist deploy keys with push access to force push.
|
||||
settings.protect_merge_whitelist_committers = Enable Merge Allowlist
|
||||
settings.protect_merge_whitelist_committers_desc = Allow only allowlisted users or teams to merge pull requests into this branch.
|
||||
settings.protect_merge_whitelist_users = Allowlisted users for merging:
|
||||
settings.protect_merge_whitelist_teams = Allowlisted teams for merging:
|
||||
settings.protect_check_status_contexts = Enable Status Check
|
||||
settings.protect_status_check_patterns = Status check patterns:
|
||||
settings.protect_status_check_patterns_desc = Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty.
|
||||
@ -2396,10 +2406,10 @@ settings.protect_invalid_status_check_pattern = Invalid status check pattern: "%
|
||||
settings.protect_no_valid_status_check_patterns = No valid status check patterns.
|
||||
settings.protect_required_approvals = Required approvals:
|
||||
settings.protect_required_approvals_desc = Allow only to merge pull request with enough positive reviews.
|
||||
settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted users or teams
|
||||
settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals.
|
||||
settings.protect_approvals_whitelist_users = Whitelisted reviewers:
|
||||
settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews:
|
||||
settings.protect_approvals_whitelist_enabled = Restrict approvals to allowlisted users or teams
|
||||
settings.protect_approvals_whitelist_enabled_desc = Only reviews from allowlisted users or teams will count to the required approvals. Without approval allowlist, reviews from anyone with write access count to the required approvals.
|
||||
settings.protect_approvals_whitelist_users = Allowlisted reviewers:
|
||||
settings.protect_approvals_whitelist_teams = Allowlisted teams for reviews:
|
||||
settings.dismiss_stale_approvals = Dismiss stale approvals
|
||||
settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed.
|
||||
settings.ignore_stale_approvals = Ignore stale approvals
|
||||
|
@ -553,6 +553,15 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
|
||||
return
|
||||
}
|
||||
forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
|
||||
return
|
||||
}
|
||||
mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
@ -571,7 +580,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
|
||||
return
|
||||
}
|
||||
var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
if err != nil {
|
||||
@ -582,6 +591,15 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
|
||||
return
|
||||
}
|
||||
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
|
||||
return
|
||||
}
|
||||
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
@ -607,8 +625,11 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
RuleName: ruleName,
|
||||
CanPush: form.EnablePush,
|
||||
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
|
||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||
WhitelistDeployKeys: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
|
||||
CanForcePush: form.EnablePush && form.EnableForcePush,
|
||||
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
|
||||
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
|
||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||
EnableStatusCheck: form.EnableStatusCheck,
|
||||
StatusCheckContexts: form.StatusCheckContexts,
|
||||
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
|
||||
@ -626,6 +647,8 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
TeamIDs: whitelistTeams,
|
||||
ForcePushUserIDs: forcePushAllowlistUsers,
|
||||
ForcePushTeamIDs: forcePushAllowlistTeams,
|
||||
MergeUserIDs: mergeWhitelistUsers,
|
||||
MergeTeamIDs: mergeWhitelistTeams,
|
||||
ApprovalsUserIDs: approvalsWhitelistUsers,
|
||||
@ -756,6 +779,27 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if form.EnableForcePush != nil {
|
||||
if !*form.EnableForcePush {
|
||||
protectBranch.CanForcePush = false
|
||||
protectBranch.EnableForcePushAllowlist = false
|
||||
protectBranch.ForcePushAllowlistDeployKeys = false
|
||||
} else {
|
||||
protectBranch.CanForcePush = true
|
||||
if form.EnableForcePushAllowlist != nil {
|
||||
if !*form.EnableForcePushAllowlist {
|
||||
protectBranch.EnableForcePushAllowlist = false
|
||||
protectBranch.ForcePushAllowlistDeployKeys = false
|
||||
} else {
|
||||
protectBranch.EnableForcePushAllowlist = true
|
||||
if form.ForcePushAllowlistDeployKeys != nil {
|
||||
protectBranch.ForcePushAllowlistDeployKeys = *form.ForcePushAllowlistDeployKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if form.EnableMergeWhitelist != nil {
|
||||
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
|
||||
}
|
||||
@ -808,7 +852,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
|
||||
}
|
||||
|
||||
var whitelistUsers []int64
|
||||
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
|
||||
if form.PushWhitelistUsernames != nil {
|
||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
|
||||
if err != nil {
|
||||
@ -822,7 +866,19 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
whitelistUsers = protectBranch.WhitelistUserIDs
|
||||
}
|
||||
var mergeWhitelistUsers []int64
|
||||
if form.ForcePushAllowlistDeployKeys != nil {
|
||||
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
forcePushAllowlistUsers = protectBranch.ForcePushAllowlistUserIDs
|
||||
}
|
||||
if form.MergeWhitelistUsernames != nil {
|
||||
mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
|
||||
if err != nil {
|
||||
@ -836,7 +892,6 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs
|
||||
}
|
||||
var approvalsWhitelistUsers []int64
|
||||
if form.ApprovalsWhitelistUsernames != nil {
|
||||
approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
|
||||
if err != nil {
|
||||
@ -851,7 +906,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
|
||||
}
|
||||
|
||||
var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
if form.PushWhitelistTeams != nil {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
@ -866,6 +921,19 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
whitelistTeams = protectBranch.WhitelistTeamIDs
|
||||
}
|
||||
if form.ForcePushAllowlistTeams != nil {
|
||||
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
forcePushAllowlistTeams = protectBranch.ForcePushAllowlistTeamIDs
|
||||
}
|
||||
if form.MergeWhitelistTeams != nil {
|
||||
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
if err != nil {
|
||||
@ -897,6 +965,8 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
TeamIDs: whitelistTeams,
|
||||
ForcePushUserIDs: forcePushAllowlistUsers,
|
||||
ForcePushTeamIDs: forcePushAllowlistTeams,
|
||||
MergeUserIDs: mergeWhitelistUsers,
|
||||
MergeTeamIDs: mergeWhitelistTeams,
|
||||
ApprovalsUserIDs: approvalsWhitelistUsers,
|
||||
|
@ -183,6 +183,8 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
return
|
||||
}
|
||||
|
||||
isForcePush := false
|
||||
|
||||
// 2. Disallow force pushes to protected branches
|
||||
if oldCommitID != objectFormat.EmptyObjectID().String() {
|
||||
output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env})
|
||||
@ -193,11 +195,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
})
|
||||
return
|
||||
} else if len(output) > 0 {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName),
|
||||
})
|
||||
return
|
||||
if protectBranch.CanForcePush {
|
||||
isForcePush = true
|
||||
} else {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,10 +250,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check if the doer is allowed to push
|
||||
// 5. Check if the doer is allowed to push (and force-push if the incoming push is a force-push)
|
||||
var canPush bool
|
||||
if ctx.opts.DeployKeyID != 0 {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
|
||||
// This flag is only ever true if protectBranch.CanForcePush is true
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableForcePushAllowlist || protectBranch.ForcePushAllowlistDeployKeys)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
|
||||
}
|
||||
} else {
|
||||
user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
@ -257,7 +268,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
})
|
||||
return
|
||||
}
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
|
||||
if isForcePush {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user)
|
||||
} else {
|
||||
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If we're not allowed to push directly
|
||||
@ -293,6 +308,13 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
}
|
||||
|
||||
// Or we're simply not able to push to this protected branch
|
||||
if isForcePush {
|
||||
log.Warn("Forbidden: User %d is not allowed to force-push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Not allowed to force-push to protected branch %s", branchName),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName),
|
||||
|
@ -77,6 +77,7 @@ func SettingsProtectedBranch(c *context.Context) {
|
||||
}
|
||||
c.Data["Users"] = users
|
||||
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
|
||||
c.Data["force_push_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistUserIDs), ",")
|
||||
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",")
|
||||
c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",")
|
||||
c.Data["status_check_contexts"] = strings.Join(rule.StatusCheckContexts, "\n")
|
||||
@ -91,6 +92,7 @@ func SettingsProtectedBranch(c *context.Context) {
|
||||
}
|
||||
c.Data["Teams"] = teams
|
||||
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",")
|
||||
c.Data["force_push_allowlist_teams"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistTeamIDs), ",")
|
||||
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",")
|
||||
c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",")
|
||||
}
|
||||
@ -149,7 +151,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
|
||||
var whitelistUsers, whitelistTeams, forcePushAllowlistUsers, forcePushAllowlistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
|
||||
protectBranch.RuleName = f.RuleName
|
||||
if f.RequiredApprovals < 0 {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
|
||||
@ -178,6 +180,27 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||
protectBranch.WhitelistDeployKeys = false
|
||||
}
|
||||
|
||||
switch f.EnableForcePush {
|
||||
case "all":
|
||||
protectBranch.CanForcePush = true
|
||||
protectBranch.EnableForcePushAllowlist = false
|
||||
protectBranch.ForcePushAllowlistDeployKeys = false
|
||||
case "whitelist":
|
||||
protectBranch.CanForcePush = true
|
||||
protectBranch.EnableForcePushAllowlist = true
|
||||
protectBranch.ForcePushAllowlistDeployKeys = f.ForcePushAllowlistDeployKeys
|
||||
if strings.TrimSpace(f.ForcePushAllowlistUsers) != "" {
|
||||
forcePushAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistUsers, ","))
|
||||
}
|
||||
if strings.TrimSpace(f.ForcePushAllowlistTeams) != "" {
|
||||
forcePushAllowlistTeams, _ = base.StringsToInt64s(strings.Split(f.ForcePushAllowlistTeams, ","))
|
||||
}
|
||||
default:
|
||||
protectBranch.CanForcePush = false
|
||||
protectBranch.EnableForcePushAllowlist = false
|
||||
protectBranch.ForcePushAllowlistDeployKeys = false
|
||||
}
|
||||
|
||||
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
|
||||
if f.EnableMergeWhitelist {
|
||||
if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
|
||||
@ -237,6 +260,8 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
TeamIDs: whitelistTeams,
|
||||
ForcePushUserIDs: forcePushAllowlistUsers,
|
||||
ForcePushTeamIDs: forcePushAllowlistTeams,
|
||||
MergeUserIDs: mergeWhitelistUsers,
|
||||
MergeTeamIDs: mergeWhitelistTeams,
|
||||
ApprovalsUserIDs: approvalsWhitelistUsers,
|
||||
|
@ -136,6 +136,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
}
|
||||
|
||||
pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs)
|
||||
forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs)
|
||||
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
|
||||
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
|
||||
|
||||
@ -145,6 +146,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
}
|
||||
|
||||
pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
|
||||
forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs)
|
||||
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
|
||||
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
|
||||
|
||||
@ -161,6 +163,11 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
PushWhitelistUsernames: pushWhitelistUsernames,
|
||||
PushWhitelistTeams: pushWhitelistTeams,
|
||||
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
|
||||
EnableForcePush: bp.CanForcePush,
|
||||
EnableForcePushAllowlist: bp.EnableForcePushAllowlist,
|
||||
ForcePushAllowlistUsernames: forcePushAllowlistUsernames,
|
||||
ForcePushAllowlistTeams: forcePushAllowlistTeams,
|
||||
ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys,
|
||||
EnableMergeWhitelist: bp.EnableMergeWhitelist,
|
||||
MergeWhitelistUsernames: mergeWhitelistUsernames,
|
||||
MergeWhitelistTeams: mergeWhitelistTeams,
|
||||
|
@ -195,6 +195,10 @@ type ProtectBranchForm struct {
|
||||
WhitelistUsers string
|
||||
WhitelistTeams string
|
||||
WhitelistDeployKeys bool
|
||||
EnableForcePush string
|
||||
ForcePushAllowlistUsers string
|
||||
ForcePushAllowlistTeams string
|
||||
ForcePushAllowlistDeployKeys bool
|
||||
EnableMergeWhitelist bool
|
||||
MergeWhitelistUsers string
|
||||
MergeWhitelistTeams string
|
||||
|
@ -117,27 +117,25 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
// can't do rebase on protected branch because need force push
|
||||
if pb == nil {
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return false, false, err
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
return false, false, nil
|
||||
}
|
||||
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
return false, false, nil
|
||||
}
|
||||
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
|
||||
return false, false, err
|
||||
}
|
||||
rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate
|
||||
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
// Update function need push permission
|
||||
rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate
|
||||
|
||||
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
|
||||
if pb != nil {
|
||||
pb.Repo = pull.BaseRepo
|
||||
if !pb.CanUserPush(ctx, user) {
|
||||
return false, false, nil
|
||||
if !pb.CanUserForcePush(ctx, user) {
|
||||
rebaseAllowed = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +94,69 @@
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.require_signed_commits_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_force_push"}}</h5>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="enable_force_push" value="none" class="toggle-target-disabled" data-target="#force_push_allowlist_box" {{if not .Rule.CanForcePush}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_disable_force_push"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_disable_force_push_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="enable_force_push" value="all" class="toggle-target-disabled" data-target="#force_push_allowlist_box" {{if and (.Rule.CanForcePush) (not .Rule.EnableForcePushAllowlist)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_all"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_all_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grouped fields">
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="enable_force_push" value="whitelist" class="toggle-target-enabled" data-target="#force_push_allowlist_box" {{if and (.Rule.CanForcePush) (.Rule.EnableForcePushAllowlist)}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_allowlist"}}</label>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.protect_enable_force_push_allowlist_desc"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="force_push_allowlist_box" class="grouped fields {{if not .Rule.EnableForcePushAllowlist}}disabled{{end}}">
|
||||
<div class="checkbox-sub-item field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_users"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="force_push_allowlist_users" value="{{.force_push_allowlist_users}}">
|
||||
<div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div>
|
||||
<div class="menu">
|
||||
{{range .Users}}
|
||||
<div class="item" data-value="{{.ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "mini"}}{{template "repo/search_name" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Owner.IsOrganization}}
|
||||
<div class="checkbox-sub-item field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_teams"}}</label>
|
||||
<div class="ui multiple search selection dropdown">
|
||||
<input type="hidden" name="force_push_allowlist_teams" value="{{.force_push_allowlist_teams}}">
|
||||
<div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div>
|
||||
<div class="menu">
|
||||
{{range .Teams}}
|
||||
<div class="item" data-value="{{.ID}}">
|
||||
{{svg "octicon-people"}}
|
||||
{{.Name}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="checkbox-sub-item field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="force_push_allowlist_deploy_keys" {{if .Rule.ForcePushAllowlistDeployKeys}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_force_push_allowlist_deploy_keys"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_pull_request_approvals"}}</h5>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_required_approvals"}}</label>
|
||||
|
78
templates/swagger/v1_json.tmpl
generated
78
templates/swagger/v1_json.tmpl
generated
@ -18714,6 +18714,14 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableApprovalsWhitelist"
|
||||
},
|
||||
"enable_force_push": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePush"
|
||||
},
|
||||
"enable_force_push_allowlist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePushAllowlist"
|
||||
},
|
||||
"enable_merge_whitelist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableMergeWhitelist"
|
||||
@ -18730,6 +18738,24 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableStatusCheck"
|
||||
},
|
||||
"force_push_allowlist_deploy_keys": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "ForcePushAllowlistDeployKeys"
|
||||
},
|
||||
"force_push_allowlist_teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistTeams"
|
||||
},
|
||||
"force_push_allowlist_usernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistUsernames"
|
||||
},
|
||||
"ignore_stale_approvals": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "IgnoreStaleApprovals"
|
||||
@ -19378,6 +19404,14 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableApprovalsWhitelist"
|
||||
},
|
||||
"enable_force_push": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePush"
|
||||
},
|
||||
"enable_force_push_allowlist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePushAllowlist"
|
||||
},
|
||||
"enable_merge_whitelist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableMergeWhitelist"
|
||||
@ -19394,6 +19428,24 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableStatusCheck"
|
||||
},
|
||||
"force_push_allowlist_deploy_keys": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "ForcePushAllowlistDeployKeys"
|
||||
},
|
||||
"force_push_allowlist_teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistTeams"
|
||||
},
|
||||
"force_push_allowlist_usernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistUsernames"
|
||||
},
|
||||
"ignore_stale_approvals": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "IgnoreStaleApprovals"
|
||||
@ -20562,6 +20614,14 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableApprovalsWhitelist"
|
||||
},
|
||||
"enable_force_push": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePush"
|
||||
},
|
||||
"enable_force_push_allowlist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableForcePushAllowlist"
|
||||
},
|
||||
"enable_merge_whitelist": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableMergeWhitelist"
|
||||
@ -20578,6 +20638,24 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "EnableStatusCheck"
|
||||
},
|
||||
"force_push_allowlist_deploy_keys": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "ForcePushAllowlistDeployKeys"
|
||||
},
|
||||
"force_push_allowlist_teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistTeams"
|
||||
},
|
||||
"force_push_allowlist_usernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "ForcePushAllowlistUsernames"
|
||||
},
|
||||
"ignore_stale_approvals": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "IgnoreStaleApprovals"
|
||||
|
@ -360,12 +360,62 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
|
||||
t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
|
||||
|
||||
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", ""))
|
||||
t.Run("GenerateCommit", func(t *testing.T) {
|
||||
|
||||
// Protect branch without any whitelisting
|
||||
t.Run("ProtectBranchNoWhitelist", func(t *testing.T) {
|
||||
doProtectBranch(ctx, "protected", "", "", "")
|
||||
})
|
||||
|
||||
// Try to push without permissions, which should fail
|
||||
t.Run("TryPushWithoutPermissions", func(t *testing.T) {
|
||||
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
|
||||
assert.NoError(t, err)
|
||||
doGitPushTestRepositoryFail(dstPath, "origin", "protected")
|
||||
})
|
||||
t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected"))
|
||||
|
||||
// Set up permissions for normal push but not force push
|
||||
t.Run("SetupNormalPushPermissions", func(t *testing.T) {
|
||||
doProtectBranch(ctx, "protected", baseCtx.Username, "", "")
|
||||
})
|
||||
|
||||
// Normal push should work
|
||||
t.Run("NormalPushWithPermissions", func(t *testing.T) {
|
||||
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
|
||||
assert.NoError(t, err)
|
||||
doGitPushTestRepository(dstPath, "origin", "protected")
|
||||
})
|
||||
|
||||
// Try to force push without force push permissions, which should fail
|
||||
t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) {
|
||||
t.Run("CreateDivergentHistory", func(t *testing.T) {
|
||||
git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
|
||||
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
|
||||
})
|
||||
|
||||
// Set up permissions for force push but not normal push
|
||||
t.Run("SetupForcePushPermissions", func(t *testing.T) {
|
||||
doProtectBranch(ctx, "protected", "", baseCtx.Username, "")
|
||||
})
|
||||
|
||||
// Try to force push without normal push permissions, which should fail
|
||||
t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) {
|
||||
doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
|
||||
})
|
||||
|
||||
// Set up permissions for normal and force push (both are required to force push)
|
||||
t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) {
|
||||
doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "")
|
||||
})
|
||||
|
||||
// Force push should now work
|
||||
t.Run("ForcePushWithPermissions", func(t *testing.T) {
|
||||
doGitPushTestRepository(dstPath, "-f", "origin", "protected")
|
||||
})
|
||||
|
||||
t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", ""))
|
||||
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
|
||||
var pr api.PullRequest
|
||||
var err error
|
||||
@ -387,14 +437,14 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
|
||||
t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
|
||||
|
||||
t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "unprotected-file-*"))
|
||||
t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
|
||||
t.Run("GenerateCommit", func(t *testing.T) {
|
||||
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
|
||||
|
||||
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, ""))
|
||||
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", ""))
|
||||
|
||||
t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
|
||||
t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
|
||||
@ -409,33 +459,37 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
|
||||
}
|
||||
}
|
||||
|
||||
func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFilePatterns string) func(t *testing.T) {
|
||||
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) {
|
||||
// We are going to just use the owner to set the protection.
|
||||
return func(t *testing.T) {
|
||||
csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
|
||||
|
||||
if userToWhitelist == "" {
|
||||
// Change branch to protected
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": branch,
|
||||
"unprotected_file_patterns": unprotectedFilePatterns,
|
||||
})
|
||||
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
} else {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelist)
|
||||
assert.NoError(t, err)
|
||||
// Change branch to protected
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": branch,
|
||||
"enable_push": "whitelist",
|
||||
"enable_whitelist": "on",
|
||||
"whitelist_users": strconv.FormatInt(user.ID, 10),
|
||||
"unprotected_file_patterns": unprotectedFilePatterns,
|
||||
})
|
||||
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
formData := map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": branch,
|
||||
"unprotected_file_patterns": unprotectedFilePatterns,
|
||||
}
|
||||
|
||||
if userToWhitelistPush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
|
||||
assert.NoError(t, err)
|
||||
formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_push"] = "whitelist"
|
||||
formData["enable_whitelist"] = "on"
|
||||
}
|
||||
|
||||
if userToWhitelistForcePush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
|
||||
assert.NoError(t, err)
|
||||
formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_force_push"] = "whitelist"
|
||||
formData["enable_force_push_allowlist"] = "on"
|
||||
}
|
||||
|
||||
// Send the request to update branch protection settings
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
|
||||
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// Check if master branch has been locked successfully
|
||||
flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
|
Loading…
Reference in New Issue
Block a user