mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-14 03:29:55 +00:00
feat: Add default PR branch update style setting (#37410)
Adds repository-level settings for pull request branch updates so admins can choose the default update method and disable merge or rebase updates. <img width="1025" height="158" src="https://github.com/user-attachments/assets/d030973b-0ddd-4035-b04f-145c445084d7" /> --------- Co-authored-by: OpenAI Codex (GPT-5) <codex@openai.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -98,11 +98,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
allowRebaseMerge := false
|
||||
allowSquash := false
|
||||
allowFastForwardOnly := false
|
||||
allowMergeUpdate := false
|
||||
allowRebaseUpdate := false
|
||||
allowManualMerge := true
|
||||
autodetectManualMerge := false
|
||||
defaultDeleteBranchAfterMerge := false
|
||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||
defaultUpdateStyle := repo_model.UpdateStyleMerge
|
||||
defaultAllowMaintainerEdit := false
|
||||
defaultTargetBranch := ""
|
||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
||||
@@ -114,11 +116,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
allowRebaseMerge = config.AllowRebaseMerge
|
||||
allowSquash = config.AllowSquash
|
||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||
allowMergeUpdate = config.AllowMergeUpdate
|
||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||
allowManualMerge = config.AllowManualMerge
|
||||
autodetectManualMerge = config.AutodetectManualMerge
|
||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||
defaultMergeStyle = config.DefaultMergeStyle
|
||||
defaultUpdateStyle = config.DefaultUpdateStyle
|
||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||
defaultTargetBranch = config.DefaultTargetBranch
|
||||
}
|
||||
@@ -240,11 +244,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
AllowRebaseMerge: allowRebaseMerge,
|
||||
AllowSquash: allowSquash,
|
||||
AllowFastForwardOnly: allowFastForwardOnly,
|
||||
AllowMergeUpdate: allowMergeUpdate,
|
||||
AllowRebaseUpdate: allowRebaseUpdate,
|
||||
AllowManualMerge: allowManualMerge,
|
||||
AutodetectManualMerge: autodetectManualMerge,
|
||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: string(defaultMergeStyle),
|
||||
DefaultUpdateStyle: string(defaultUpdateStyle),
|
||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||
DefaultTargetBranch: defaultTargetBranch,
|
||||
AvatarURL: repo.AvatarLink(ctx),
|
||||
|
||||
@@ -142,7 +142,9 @@ type RepoSettingForm struct {
|
||||
PullsAllowManualMerge bool
|
||||
PullsDefaultMergeStyle string
|
||||
EnableAutodetectManualMerge bool
|
||||
PullsAllowMergeUpdate bool
|
||||
PullsAllowRebaseUpdate bool
|
||||
PullsDefaultUpdateStyle string
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultAllowMaintainerEdit bool
|
||||
DefaultTargetBranch string
|
||||
|
||||
+31
-23
@@ -131,74 +131,82 @@ func isUserAllowedToPushOrForcePushInRepoBranch(ctx context.Context, user *user_
|
||||
return pushAllowed, forcePushAllowed, nil
|
||||
}
|
||||
|
||||
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
|
||||
// CheckUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
|
||||
// update PR means send new commits to PR head branch from base branch
|
||||
func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (pushAllowed, rebaseAllowed bool, err error) {
|
||||
func CheckUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (ret struct {
|
||||
MergeAllowed, RebaseAllowed bool
|
||||
DefaultUpdateStyle repo_model.UpdateStyle
|
||||
}, err error,
|
||||
) {
|
||||
if user == nil {
|
||||
return false, false, nil
|
||||
return ret, nil
|
||||
}
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// 1. check whether pull request enabled.
|
||||
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
return false, false, nil // the PR unit is disabled in base repo means no update allowed
|
||||
return ret, nil // the PR unit is disabled in base repo means no update allowed
|
||||
} else if err != nil {
|
||||
return false, false, fmt.Errorf("get base repo unit: %v", err)
|
||||
return ret, fmt.Errorf("get base repo unit: %v", err)
|
||||
}
|
||||
|
||||
// 2. only support Github style pull request
|
||||
if pull.Flow == issues_model.PullRequestFlowAGit {
|
||||
return false, false, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// 3. check user push permission on head repository
|
||||
pushAllowed, rebaseAllowed, err = isUserAllowedToPushOrForcePushInRepoBranch(ctx, user, pull.HeadRepo, pull.HeadBranch)
|
||||
ret.MergeAllowed, ret.RebaseAllowed, err = isUserAllowedToPushOrForcePushInRepoBranch(ctx, user, pull.HeadRepo, pull.HeadBranch)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// 4. if the pull creator allows maintainer to edit, we need to check whether
|
||||
// user is a maintainer (has permission to merge into base branch) and inherit pull request poster's permission
|
||||
if pull.AllowMaintainerEdit && (!pushAllowed || !rebaseAllowed) {
|
||||
if pull.AllowMaintainerEdit && (!ret.MergeAllowed || !ret.RebaseAllowed) {
|
||||
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, user)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
userAllowedToMergePR, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
if userAllowedToMergePR {
|
||||
// the user is maintainer (can merge PR), and this PR is allowed to be edited by maintainers,
|
||||
// then the user should inherit the PR poster's push/rebase permission for the head branch
|
||||
if err := pull.LoadIssue(ctx); err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
if err := pull.Issue.LoadPoster(ctx); err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
posterPushAllowed, posterRebaseAllowed, err := isUserAllowedToPushOrForcePushInRepoBranch(ctx, pull.Issue.Poster, pull.HeadRepo, pull.HeadBranch)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
return ret, err
|
||||
}
|
||||
if !pushAllowed {
|
||||
pushAllowed = posterPushAllowed
|
||||
if !ret.MergeAllowed {
|
||||
ret.MergeAllowed = posterPushAllowed
|
||||
}
|
||||
if !rebaseAllowed {
|
||||
rebaseAllowed = posterRebaseAllowed
|
||||
if !ret.RebaseAllowed {
|
||||
ret.RebaseAllowed = posterRebaseAllowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. check base repository's AllowRebaseUpdate configuration
|
||||
// it is a config in base repo but controls the head (fork) repo's "Update" behavior
|
||||
return pushAllowed, rebaseAllowed && prBaseUnit.PullRequestsConfig().AllowRebaseUpdate, nil
|
||||
// 5. apply base repository's update configuration; it is a config on the base repo,
|
||||
// but it controls the head (fork) repo's "Update" behavior.
|
||||
prConfig := prBaseUnit.PullRequestsConfig()
|
||||
ret.MergeAllowed = ret.MergeAllowed && prConfig.AllowMergeUpdate
|
||||
ret.RebaseAllowed = ret.RebaseAllowed && prConfig.AllowRebaseUpdate
|
||||
ret.DefaultUpdateStyle = prConfig.DefaultUpdateStyle
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func syncCommitDivergence(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -23,11 +24,21 @@ import (
|
||||
func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
setRepoAllowRebaseUpdate := func(t *testing.T, repoID int64, allow bool) {
|
||||
updatePRConfig := func(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) {
|
||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
|
||||
repoUnit.PullRequestsConfig().AllowRebaseUpdate = allow
|
||||
update(repoUnit.PullRequestsConfig())
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
||||
}
|
||||
setRepoAllowRebaseUpdate := func(t *testing.T, repoID int64, allow bool) {
|
||||
updatePRConfig(t, repoID, func(c *repo_model.PullRequestsConfig) { c.AllowRebaseUpdate = allow })
|
||||
}
|
||||
setRepoAllowMergeUpdate := func(t *testing.T, repoID int64, allow bool) {
|
||||
updatePRConfig(t, repoID, func(c *repo_model.PullRequestsConfig) { c.AllowMergeUpdate = allow })
|
||||
}
|
||||
checkUserAllowedToUpdate := func(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (bool, bool, repo_model.UpdateStyle, error) {
|
||||
ret, err := CheckUserAllowedToUpdate(ctx, pull, user)
|
||||
return ret.MergeAllowed, ret.RebaseAllowed, ret.DefaultUpdateStyle, err
|
||||
}
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
@@ -43,21 +54,33 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer db.DeleteByBean(t.Context(), protectedBranch)
|
||||
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
pushAllowed, rebaseAllowed, defaultMergeStyle, err := checkUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
assert.Equal(t, repo_model.UpdateStyleMerge, defaultMergeStyle)
|
||||
})
|
||||
|
||||
t.Run("DisallowRebaseWhenConfigDisabled", func(t *testing.T) {
|
||||
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
setRepoAllowRebaseUpdate(t, pr2.BaseRepoID, false)
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
})
|
||||
|
||||
t.Run("DisallowMergeWhenConfigDisabled", func(t *testing.T) {
|
||||
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
setRepoAllowRebaseUpdate(t, pr2.BaseRepoID, true)
|
||||
setRepoAllowMergeUpdate(t, pr2.BaseRepoID, false)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, pushAllowed)
|
||||
assert.True(t, rebaseAllowed)
|
||||
setRepoAllowMergeUpdate(t, pr2.BaseRepoID, true)
|
||||
})
|
||||
|
||||
t.Run("ReadOnlyAccessDenied", func(t *testing.T) {
|
||||
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
@@ -73,7 +96,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
require.NoError(t, pr2.LoadHeadRepo(t.Context()))
|
||||
assert.NoError(t, access_model.RecalculateUserAccess(t.Context(), pr2.HeadRepo, user4.ID))
|
||||
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr2, user4)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr2, user4)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
@@ -91,7 +114,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer db.DeleteByBean(t.Context(), protectedBranch)
|
||||
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr2, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
@@ -102,7 +125,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
t.Run("MaintainerEditRespectsPosterPermissions", func(t *testing.T) {
|
||||
pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
pr3.AllowMaintainerEdit = true
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
@@ -132,7 +155,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
require.NoError(t, pr3.LoadHeadRepo(t.Context()))
|
||||
assert.NoError(t, access_model.RecalculateUserAccess(t.Context(), pr3.HeadRepo, pr3Poster.ID))
|
||||
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pushAllowed)
|
||||
assert.True(t, rebaseAllowed)
|
||||
@@ -164,7 +187,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
|
||||
setRepoAllowRebaseUpdate(t, pr3.BaseRepoID, false)
|
||||
|
||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pushAllowed)
|
||||
assert.False(t, rebaseAllowed)
|
||||
|
||||
Reference in New Issue
Block a user