Skip to content

Allow force push to protected branches #28086

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

Merged
merged 48 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
50819b0
Allow force pushes to protected branches
Nov 16, 2023
f610efa
Updating label for whitelist deploy keys force push
Nov 16, 2023
9da0dc1
Update RemoveUserIDFromProtectedBranch for forcepush user/team IDs
Nov 16, 2023
7c6cd8b
Make fmt whitespace fix
Nov 16, 2023
2d2bbf2
Update strings for clarity
Nov 16, 2023
f28ecbe
Fix backend dup code lint err and generate swagger
Nov 24, 2023
fdbe77f
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Dec 14, 2023
be622e5
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Dec 19, 2023
cb057ff
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Jan 11, 2024
21cb9bc
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Jan 15, 2024
d102809
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Jan 31, 2024
5896945
Use slices.Contains instead of base.Int64sContains
Jan 31, 2024
945ea90
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Feb 2, 2024
a06a3e9
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Feb 2, 2024
9968020
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Feb 17, 2024
622e87c
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Apr 5, 2024
bf135f1
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Apr 13, 2024
11acb60
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Apr 19, 2024
a4d344c
Fix whitespace err
Apr 19, 2024
d038bc5
Make format
Apr 19, 2024
b58a3af
Remove debug log
Apr 19, 2024
dd2b74d
Fix typo in logging
Apr 19, 2024
a55d217
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman May 7, 2024
b85e559
Add initial force push tests (just git, no pr test)
May 8, 2024
d37d1c5
Fix test error, incorrect function param order
May 8, 2024
9d5b5fb
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman May 8, 2024
e7816e6
Improve force push test with explicit divergent history (otherwise gi…
May 9, 2024
7aca890
Add testcase for force pushing without normal push permission
May 9, 2024
d399196
Remove redundant locale str
May 9, 2024
812b08f
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman May 18, 2024
fe8778a
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Jun 30, 2024
9475e8c
Merge branch 'main' into allow-force-push-protected-branches
henrygoodman Jul 4, 2024
72d7c44
Rename ProtectedBranch struct fields for ids, change whitelist to all…
Jul 4, 2024
98add19
Add db migration v1.23 as v300
Jul 4, 2024
b3b3bd4
Update ProtectedBranch struct xorm tags
Jul 4, 2024
410eb59
Update migration for ProtectedBranch struct fields
Jul 5, 2024
be927f0
Update models/git/protected_branch.go
wxiaoguang Jul 5, 2024
933061a
Update models/git/protected_branch.go
wxiaoguang Jul 5, 2024
939a927
Update protected_branch.go
wxiaoguang Jul 5, 2024
8ad513c
Update protected_branch.go
wxiaoguang Jul 5, 2024
7e11c6e
Update v300.go
wxiaoguang Jul 5, 2024
9390d6f
Update v300.go
wxiaoguang Jul 5, 2024
1c81cb2
Update protected_branch.go
wxiaoguang Jul 5, 2024
79ee011
Update protected_branch.go
wxiaoguang Jul 5, 2024
a2cccd3
Remove explicit xorm column names
Jul 5, 2024
4546f15
Update v300.go
wxiaoguang Jul 5, 2024
534a1fe
Update remaining new whitelist -> allowlist refs
Jul 5, 2024
6b93981
Merge branch 'main' into allow-force-push-protected-branches
GiteaBot Jul 5, 2024
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
124 changes: 93 additions & 31 deletions models/git/protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -301,6 +333,9 @@ type WhitelistOptions struct {
UserIDs []int64
TeamIDs []int64

ForcePushUserIDs []int64
ForcePushTeamIDs []int64

MergeUserIDs []int64
MergeTeamIDs []int64

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
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 {
// 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
}

// RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options
// RemoveUserIDFromProtectedBranch removes all user ids from protected branch options
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
columnNames := []string{
"whitelist_user_i_ds",
"force_push_allowlist_user_i_ds",
"merge_whitelist_user_i_ds",
"approvals_whitelist_user_i_ds",
}
return removeIDsFromProtectedBranch(ctx, p, userID, 0, columnNames)
}

// 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)
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions models/migrations/v1_23/v300.go
Original file line number Diff line number Diff line change
@@ -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))
}
15 changes: 15 additions & 0 deletions modules/structs/repo_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
36 changes: 23 additions & 13 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading