mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-10 05:24:18 +09: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:
co-authored by
OpenAI Codex
silverwind
Claude
wxiaoguang
parent
16189a68c4
commit
34fd3c9f06
@@ -29,6 +29,16 @@ const (
|
|||||||
MergeStyleRebaseUpdate MergeStyle = "rebase-update-only"
|
MergeStyleRebaseUpdate MergeStyle = "rebase-update-only"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateStyle is a pull request branch update style
|
||||||
|
type UpdateStyle string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UpdateStyleMerge merges the base branch into the pull request branch
|
||||||
|
UpdateStyleMerge UpdateStyle = "merge"
|
||||||
|
// UpdateStyleRebase rebases the pull request branch onto the base branch
|
||||||
|
UpdateStyleRebase UpdateStyle = "rebase"
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateDefaultBranch updates the default branch
|
// UpdateDefaultBranch updates the default branch
|
||||||
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
|
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
|
||||||
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
|
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -30,3 +31,82 @@ func TestDefaultTargetBranchSelection(t *testing.T) {
|
|||||||
repo.Units = nil
|
repo.Units = nil
|
||||||
assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx))
|
assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullRequestConfigFromDB(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
// name describes the row shape under test; the comments capture why each row matters.
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
wantMergeUpdate bool
|
||||||
|
wantRebaseUpdate bool
|
||||||
|
wantDefaultStyle UpdateStyle
|
||||||
|
wantValidatesPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Empty object exercises the all-defaults path (e.g. fresh repos created via low-level paths).
|
||||||
|
name: "defaults", json: "{}",
|
||||||
|
wantMergeUpdate: true, wantRebaseUpdate: true,
|
||||||
|
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Realistic upgrade case: pre-PR JSON lacks the new fields and has AllowRebaseUpdate=false.
|
||||||
|
// Historical setting must be preserved while new fields take safe defaults.
|
||||||
|
name: "legacy without new fields",
|
||||||
|
json: `{"AllowMerge":true,"AllowRebase":true,"AllowRebaseMerge":true,"AllowSquash":true,"AllowRebaseUpdate":false}`,
|
||||||
|
wantMergeUpdate: true, wantRebaseUpdate: false,
|
||||||
|
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Partially-migrated row with explicit empty string must normalize so ValidateUpdateSettings passes.
|
||||||
|
name: "empty default style", json: `{"DefaultUpdateStyle":""}`,
|
||||||
|
wantMergeUpdate: true, wantRebaseUpdate: true,
|
||||||
|
wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cfg := new(PullRequestsConfig)
|
||||||
|
assert.NoError(t, cfg.FromDB([]byte(tc.json)))
|
||||||
|
assert.Equal(t, tc.wantMergeUpdate, cfg.AllowMergeUpdate)
|
||||||
|
assert.Equal(t, tc.wantRebaseUpdate, cfg.AllowRebaseUpdate)
|
||||||
|
assert.Equal(t, tc.wantDefaultStyle, cfg.DefaultUpdateStyle)
|
||||||
|
if tc.wantValidatesPass {
|
||||||
|
assert.NoError(t, cfg.ValidateUpdateSettings())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullRequestConfigValidateUpdateSettingsInvalidArgument(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
cfg PullRequestsConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid default style",
|
||||||
|
cfg: PullRequestsConfig{
|
||||||
|
AllowMergeUpdate: true,
|
||||||
|
AllowRebaseUpdate: true,
|
||||||
|
DefaultUpdateStyle: "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no update style enabled",
|
||||||
|
cfg: PullRequestsConfig{
|
||||||
|
DefaultUpdateStyle: UpdateStyleMerge,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default update style disabled",
|
||||||
|
cfg: PullRequestsConfig{
|
||||||
|
AllowRebaseUpdate: true,
|
||||||
|
DefaultUpdateStyle: UpdateStyleMerge,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.ErrorIs(t, tc.cfg.ValidateUpdateSettings(), util.ErrInvalidArgument)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ type PullRequestsConfig struct {
|
|||||||
AllowFastForwardOnly bool
|
AllowFastForwardOnly bool
|
||||||
AllowManualMerge bool
|
AllowManualMerge bool
|
||||||
AutodetectManualMerge bool
|
AutodetectManualMerge bool
|
||||||
|
AllowMergeUpdate bool
|
||||||
AllowRebaseUpdate bool
|
AllowRebaseUpdate bool
|
||||||
|
DefaultUpdateStyle UpdateStyle
|
||||||
DefaultDeleteBranchAfterMerge bool
|
DefaultDeleteBranchAfterMerge bool
|
||||||
DefaultMergeStyle MergeStyle
|
DefaultMergeStyle MergeStyle
|
||||||
DefaultAllowMaintainerEdit bool
|
DefaultAllowMaintainerEdit bool
|
||||||
@@ -139,7 +141,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
|||||||
AllowRebaseMerge: true,
|
AllowRebaseMerge: true,
|
||||||
AllowSquash: true,
|
AllowSquash: true,
|
||||||
AllowFastForwardOnly: true,
|
AllowFastForwardOnly: true,
|
||||||
|
AllowMergeUpdate: true,
|
||||||
AllowRebaseUpdate: true,
|
AllowRebaseUpdate: true,
|
||||||
|
DefaultUpdateStyle: UpdateStyleMerge,
|
||||||
DefaultAllowMaintainerEdit: true,
|
DefaultAllowMaintainerEdit: true,
|
||||||
}
|
}
|
||||||
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
||||||
@@ -152,7 +156,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
|||||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||||
// set default values for existing PullRequestConfig in DB
|
// set default values for existing PullRequestConfig in DB
|
||||||
*cfg = *DefaultPullRequestsConfig()
|
*cfg = *DefaultPullRequestsConfig()
|
||||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
|
||||||
|
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToDB exports a PullRequestsConfig to a serialized format.
|
// ToDB exports a PullRequestsConfig to a serialized format.
|
||||||
@@ -170,6 +176,32 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
|
|||||||
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
|
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsUpdateStyleAllowed returns if a pull request branch update style is allowed
|
||||||
|
func (cfg *PullRequestsConfig) IsUpdateStyleAllowed(updateStyle UpdateStyle) bool {
|
||||||
|
switch updateStyle {
|
||||||
|
case UpdateStyleMerge:
|
||||||
|
return cfg.AllowMergeUpdate
|
||||||
|
case UpdateStyleRebase:
|
||||||
|
return cfg.AllowRebaseUpdate
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUpdateSettings checks that the AllowMerge/RebaseUpdate flags and DefaultUpdateStyle are mutually consistent.
|
||||||
|
func (cfg *PullRequestsConfig) ValidateUpdateSettings() error {
|
||||||
|
if cfg.DefaultUpdateStyle != UpdateStyleMerge && cfg.DefaultUpdateStyle != UpdateStyleRebase {
|
||||||
|
return util.NewInvalidArgumentErrorf("default update style must be merge or rebase")
|
||||||
|
}
|
||||||
|
if !cfg.AllowMergeUpdate && !cfg.AllowRebaseUpdate {
|
||||||
|
return util.NewInvalidArgumentErrorf("at least one pull request branch update style must be enabled")
|
||||||
|
}
|
||||||
|
if !cfg.IsUpdateStyleAllowed(cfg.DefaultUpdateStyle) {
|
||||||
|
return util.NewInvalidArgumentErrorf("default update style must be enabled")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultPullRequestsUnit(repoID int64) RepoUnit {
|
func DefaultPullRequestsUnit(repoID int64) RepoUnit {
|
||||||
return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
|
return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,11 +113,13 @@ type Repository struct {
|
|||||||
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
|
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
|
||||||
AllowSquash bool `json:"allow_squash_merge"`
|
AllowSquash bool `json:"allow_squash_merge"`
|
||||||
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
||||||
|
AllowMergeUpdate bool `json:"allow_merge_update"`
|
||||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||||
AllowManualMerge bool `json:"allow_manual_merge"`
|
AllowManualMerge bool `json:"allow_manual_merge"`
|
||||||
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
||||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||||
DefaultMergeStyle string `json:"default_merge_style"`
|
DefaultMergeStyle string `json:"default_merge_style"`
|
||||||
|
DefaultUpdateStyle string `json:"default_update_style"`
|
||||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
Internal bool `json:"internal"`
|
Internal bool `json:"internal"`
|
||||||
@@ -224,12 +226,16 @@ type EditRepoOption struct {
|
|||||||
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
|
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
|
||||||
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
|
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
|
||||||
AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"`
|
AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"`
|
||||||
|
// either `true` to allow updating pull request branch by merge, or `false` to prevent it.
|
||||||
|
AllowMergeUpdate *bool `json:"allow_merge_update,omitempty"`
|
||||||
// either `true` to allow updating pull request branch by rebase, or `false` to prevent it.
|
// either `true` to allow updating pull request branch by rebase, or `false` to prevent it.
|
||||||
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
|
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
|
||||||
// set to `true` to delete pr branch after merge by default
|
// set to `true` to delete pr branch after merge by default
|
||||||
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
|
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
|
||||||
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
|
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
|
||||||
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
||||||
|
// set to an update style to be used by this repository: "merge" or "rebase".
|
||||||
|
DefaultUpdateStyle *string `json:"default_update_style,omitempty"`
|
||||||
// set to `true` to allow edits from maintainers by default
|
// set to `true` to allow edits from maintainers by default
|
||||||
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
||||||
// set to `true` to archive this repository.
|
// set to `true` to archive this repository.
|
||||||
|
|||||||
@@ -2138,7 +2138,9 @@
|
|||||||
"repo.settings.pulls_desc": "Enable Repository Pull Requests",
|
"repo.settings.pulls_desc": "Enable Repository Pull Requests",
|
||||||
"repo.settings.pulls.ignore_whitespace": "Ignore Whitespace for Conflicts",
|
"repo.settings.pulls.ignore_whitespace": "Ignore Whitespace for Conflicts",
|
||||||
"repo.settings.pulls.enable_autodetect_manual_merge": "Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)",
|
"repo.settings.pulls.enable_autodetect_manual_merge": "Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)",
|
||||||
|
"repo.settings.pulls.allow_merge_update": "Enable updating pull request branch by merge",
|
||||||
"repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase",
|
"repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase",
|
||||||
|
"repo.settings.pulls.default_update_style": "Default branch update style",
|
||||||
"repo.settings.pulls.default_target_branch": "Default target branch for new pull requests",
|
"repo.settings.pulls.default_target_branch": "Default target branch for new pull requests",
|
||||||
"repo.settings.pulls.default_target_branch_default": "Default branch (%s)",
|
"repo.settings.pulls.default_target_branch_default": "Default branch (%s)",
|
||||||
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
||||||
|
|||||||
@@ -1253,15 +1253,17 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rebase := ctx.FormString("style") == "rebase"
|
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
|
||||||
|
// so existing API clients keep getting a merge update.
|
||||||
|
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
|
||||||
|
|
||||||
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
|
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -885,10 +885,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
|||||||
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
|
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
|
||||||
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
|
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
|
||||||
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
|
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
|
||||||
|
optional.AssignPtrValue(changed, &config.AllowMergeUpdate, opts.AllowMergeUpdate)
|
||||||
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
|
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
|
||||||
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
|
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
|
||||||
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
||||||
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
|
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
|
||||||
|
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
|
||||||
|
// only validate update-style fields when the caller is actually changing one of them,
|
||||||
|
// so unrelated PATCH calls don't reject historical configs.
|
||||||
|
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
|
||||||
|
if err := config.ValidateUpdateSettings(); err != nil {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if *changed || mustInsertPullRequestUnit {
|
if *changed || mustInsertPullRequestUnit {
|
||||||
units = append(units, repo_model.RepoUnit{
|
units = append(units, repo_model.RepoUnit{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
|||||||
@@ -867,10 +867,8 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
|
|||||||
|
|
||||||
if !prInfo.IsPullRequestBroken {
|
if !prInfo.IsPullRequestBroken {
|
||||||
data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken
|
data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken
|
||||||
var err error
|
prInfo.preparePullUpdateActions(ctx)
|
||||||
data.UpdateAllowed, data.UpdateByRebaseAllowed, err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
|
if ctx.Written() {
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("IsUserAllowedToUpdate", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,6 +973,45 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
|
|||||||
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
|
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (prInfo *pullRequestViewInfo) preparePullUpdateActions(ctx *context.Context) {
|
||||||
|
pull := prInfo.issue.PullRequest
|
||||||
|
data := prInfo.MergeBoxData
|
||||||
|
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pull, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsUserAllowedToUpdate", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !userUpdateStyles.MergeAllowed && !userUpdateStyles.RebaseAllowed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issueLink := prInfo.issue.Link()
|
||||||
|
mergeAction := &pullUpdateAction{
|
||||||
|
URL: issueLink + "/update?style=merge",
|
||||||
|
Text: ctx.Tr("repo.pulls.update_branch"),
|
||||||
|
}
|
||||||
|
rebaseAction := &pullUpdateAction{
|
||||||
|
URL: issueLink + "/update?style=rebase",
|
||||||
|
Text: ctx.Tr("repo.pulls.update_branch_rebase"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateStyles.MergeAllowed {
|
||||||
|
data.UpdateStyleOptions = append(data.UpdateStyleOptions, mergeAction)
|
||||||
|
}
|
||||||
|
if userUpdateStyles.RebaseAllowed {
|
||||||
|
data.UpdateStyleOptions = append(data.UpdateStyleOptions, rebaseAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateStyles.DefaultUpdateStyle == repo_model.UpdateStyleRebase && userUpdateStyles.RebaseAllowed {
|
||||||
|
data.UpdatePrimaryAction = rebaseAction
|
||||||
|
} else if userUpdateStyles.DefaultUpdateStyle == repo_model.UpdateStyleMerge && userUpdateStyles.MergeAllowed {
|
||||||
|
data.UpdatePrimaryAction = mergeAction
|
||||||
|
} else {
|
||||||
|
data.UpdatePrimaryAction = data.UpdateStyleOptions[0]
|
||||||
|
}
|
||||||
|
data.UpdatePrimaryAction.Selected = true
|
||||||
|
}
|
||||||
|
|
||||||
func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectionChecks(ctx *context.Context) {
|
func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectionChecks(ctx *context.Context) {
|
||||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch)
|
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -280,9 +280,9 @@ type pullMergeBoxData struct {
|
|||||||
CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers
|
CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers
|
||||||
allowMerge bool // doer has permission to merge
|
allowMerge bool // doer has permission to merge
|
||||||
|
|
||||||
ShowUpdatePullInfo bool
|
ShowUpdatePullInfo bool
|
||||||
UpdateAllowed bool
|
UpdatePrimaryAction *pullUpdateAction
|
||||||
UpdateByRebaseAllowed bool
|
UpdateStyleOptions []*pullUpdateAction
|
||||||
|
|
||||||
MergeFormProps map[string]any
|
MergeFormProps map[string]any
|
||||||
ShowPullCommands bool
|
ShowPullCommands bool
|
||||||
@@ -308,6 +308,12 @@ type pullMergeBoxData struct {
|
|||||||
InfoSections []*pullInfoSection
|
InfoSections []*pullInfoSection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pullUpdateAction struct {
|
||||||
|
URL string
|
||||||
|
Text template.HTML
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
// pullRequestViewInfo is a structured type for viewing pull request
|
// pullRequestViewInfo is a structured type for viewing pull request
|
||||||
// Refactoring plan:
|
// Refactoring plan:
|
||||||
// * move dynamic template-data-based variable into this struct
|
// * move dynamic template-data-based variable into this struct
|
||||||
@@ -957,8 +963,6 @@ func UpdatePullRequest(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rebase := ctx.FormString("style") == "rebase"
|
|
||||||
|
|
||||||
if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
|
if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
|
||||||
ctx.ServerError("LoadBaseRepo", err)
|
ctx.ServerError("LoadBaseRepo", err)
|
||||||
return
|
return
|
||||||
@@ -968,13 +972,14 @@ func UpdatePullRequest(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer)
|
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("IsUserAllowedToMerge", err)
|
ctx.ServerError("IsUserAllowedToMerge", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
|
rebase := ctx.FormString("style", string(userUpdateStyles.DefaultUpdateStyle)) == string(repo_model.UpdateStyleRebase)
|
||||||
|
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
|
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package setting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -49,6 +50,12 @@ const (
|
|||||||
tplDeployKeys templates.TplName = "repo/settings/deploy_keys"
|
tplDeployKeys templates.TplName = "repo/settings/deploy_keys"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type selectOption struct {
|
||||||
|
Value string
|
||||||
|
Text template.HTML
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
// SettingsCtxData is a middleware that sets all the general context data for the
|
// SettingsCtxData is a middleware that sets all the general context data for the
|
||||||
// settings template.
|
// settings template.
|
||||||
func SettingsCtxData(ctx *context.Context) {
|
func SettingsCtxData(ctx *context.Context) {
|
||||||
@@ -66,6 +73,7 @@ func SettingsCtxData(ctx *context.Context) {
|
|||||||
ctx.Data["SigningKeyAvailable"] = signing != nil
|
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||||
|
preparePullRequestSettings(ctx)
|
||||||
|
|
||||||
if ctx.Doer.IsAdmin {
|
if ctx.Doer.IsAdmin {
|
||||||
if setting.Indexer.RepoIndexerEnabled {
|
if setting.Indexer.RepoIndexerEnabled {
|
||||||
@@ -96,6 +104,35 @@ func SettingsCtxData(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func preparePullRequestSettings(ctx *context.Context) {
|
||||||
|
defaultUpdateStyle := repo_model.UpdateStyleMerge
|
||||||
|
if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
|
||||||
|
prUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit_model.TypePullRequests)
|
||||||
|
defaultUpdateStyle = util.IfZero(prUnit.PullRequestsConfig().DefaultUpdateStyle, repo_model.UpdateStyleMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBranchText := ctx.Tr("repo.pulls.update_branch")
|
||||||
|
rebaseUpdateText := ctx.Tr("repo.pulls.update_branch_rebase")
|
||||||
|
defaultUpdateStyleText := updateBranchText
|
||||||
|
if defaultUpdateStyle == repo_model.UpdateStyleRebase {
|
||||||
|
defaultUpdateStyleText = rebaseUpdateText
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["PullsDefaultUpdateStyleText"] = defaultUpdateStyleText
|
||||||
|
ctx.Data["PullsDefaultUpdateStyleOptions"] = []selectOption{
|
||||||
|
{
|
||||||
|
Value: string(repo_model.UpdateStyleMerge),
|
||||||
|
Text: updateBranchText,
|
||||||
|
Selected: defaultUpdateStyle == repo_model.UpdateStyleMerge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: string(repo_model.UpdateStyleRebase),
|
||||||
|
Text: rebaseUpdateText,
|
||||||
|
Selected: defaultUpdateStyle == repo_model.UpdateStyleRebase,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Settings show a repository's settings page
|
// Settings show a repository's settings page
|
||||||
func Settings(ctx *context.Context) {
|
func Settings(ctx *context.Context) {
|
||||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||||
@@ -616,7 +653,8 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||||
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
|
defaultUpdateStyle := util.IfZero(repo_model.UpdateStyle(form.PullsDefaultUpdateStyle), repo_model.UpdateStyleMerge)
|
||||||
|
prConfig := &repo_model.PullRequestsConfig{
|
||||||
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
|
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
|
||||||
AllowMerge: form.PullsAllowMerge,
|
AllowMerge: form.PullsAllowMerge,
|
||||||
AllowRebase: form.PullsAllowRebase,
|
AllowRebase: form.PullsAllowRebase,
|
||||||
@@ -625,12 +663,20 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
|||||||
AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
|
AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
|
||||||
AllowManualMerge: form.PullsAllowManualMerge,
|
AllowManualMerge: form.PullsAllowManualMerge,
|
||||||
AutodetectManualMerge: form.EnableAutodetectManualMerge,
|
AutodetectManualMerge: form.EnableAutodetectManualMerge,
|
||||||
|
AllowMergeUpdate: form.PullsAllowMergeUpdate,
|
||||||
AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
|
AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
|
||||||
|
DefaultUpdateStyle: defaultUpdateStyle,
|
||||||
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
||||||
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
||||||
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
||||||
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
|
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
|
||||||
}))
|
}
|
||||||
|
if err := prConfig.ValidateUpdateSettings(); err != nil {
|
||||||
|
ctx.Flash.Error(err.Error())
|
||||||
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
|
||||||
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
allowRebaseMerge := false
|
allowRebaseMerge := false
|
||||||
allowSquash := false
|
allowSquash := false
|
||||||
allowFastForwardOnly := false
|
allowFastForwardOnly := false
|
||||||
|
allowMergeUpdate := false
|
||||||
allowRebaseUpdate := false
|
allowRebaseUpdate := false
|
||||||
allowManualMerge := true
|
allowManualMerge := true
|
||||||
autodetectManualMerge := false
|
autodetectManualMerge := false
|
||||||
defaultDeleteBranchAfterMerge := false
|
defaultDeleteBranchAfterMerge := false
|
||||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||||
|
defaultUpdateStyle := repo_model.UpdateStyleMerge
|
||||||
defaultAllowMaintainerEdit := false
|
defaultAllowMaintainerEdit := false
|
||||||
defaultTargetBranch := ""
|
defaultTargetBranch := ""
|
||||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
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
|
allowRebaseMerge = config.AllowRebaseMerge
|
||||||
allowSquash = config.AllowSquash
|
allowSquash = config.AllowSquash
|
||||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||||
|
allowMergeUpdate = config.AllowMergeUpdate
|
||||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||||
allowManualMerge = config.AllowManualMerge
|
allowManualMerge = config.AllowManualMerge
|
||||||
autodetectManualMerge = config.AutodetectManualMerge
|
autodetectManualMerge = config.AutodetectManualMerge
|
||||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||||
defaultMergeStyle = config.DefaultMergeStyle
|
defaultMergeStyle = config.DefaultMergeStyle
|
||||||
|
defaultUpdateStyle = config.DefaultUpdateStyle
|
||||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||||
defaultTargetBranch = config.DefaultTargetBranch
|
defaultTargetBranch = config.DefaultTargetBranch
|
||||||
}
|
}
|
||||||
@@ -240,11 +244,13 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
AllowRebaseMerge: allowRebaseMerge,
|
AllowRebaseMerge: allowRebaseMerge,
|
||||||
AllowSquash: allowSquash,
|
AllowSquash: allowSquash,
|
||||||
AllowFastForwardOnly: allowFastForwardOnly,
|
AllowFastForwardOnly: allowFastForwardOnly,
|
||||||
|
AllowMergeUpdate: allowMergeUpdate,
|
||||||
AllowRebaseUpdate: allowRebaseUpdate,
|
AllowRebaseUpdate: allowRebaseUpdate,
|
||||||
AllowManualMerge: allowManualMerge,
|
AllowManualMerge: allowManualMerge,
|
||||||
AutodetectManualMerge: autodetectManualMerge,
|
AutodetectManualMerge: autodetectManualMerge,
|
||||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||||
DefaultMergeStyle: string(defaultMergeStyle),
|
DefaultMergeStyle: string(defaultMergeStyle),
|
||||||
|
DefaultUpdateStyle: string(defaultUpdateStyle),
|
||||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||||
DefaultTargetBranch: defaultTargetBranch,
|
DefaultTargetBranch: defaultTargetBranch,
|
||||||
AvatarURL: repo.AvatarLink(ctx),
|
AvatarURL: repo.AvatarLink(ctx),
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ type RepoSettingForm struct {
|
|||||||
PullsAllowManualMerge bool
|
PullsAllowManualMerge bool
|
||||||
PullsDefaultMergeStyle string
|
PullsDefaultMergeStyle string
|
||||||
EnableAutodetectManualMerge bool
|
EnableAutodetectManualMerge bool
|
||||||
|
PullsAllowMergeUpdate bool
|
||||||
PullsAllowRebaseUpdate bool
|
PullsAllowRebaseUpdate bool
|
||||||
|
PullsDefaultUpdateStyle string
|
||||||
DefaultDeleteBranchAfterMerge bool
|
DefaultDeleteBranchAfterMerge bool
|
||||||
DefaultAllowMaintainerEdit bool
|
DefaultAllowMaintainerEdit bool
|
||||||
DefaultTargetBranch string
|
DefaultTargetBranch string
|
||||||
|
|||||||
+31
-23
@@ -131,74 +131,82 @@ func isUserAllowedToPushOrForcePushInRepoBranch(ctx context.Context, user *user_
|
|||||||
return pushAllowed, forcePushAllowed, nil
|
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
|
// 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 {
|
if user == nil {
|
||||||
return false, false, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. check whether pull request enabled.
|
// 1. check whether pull request enabled.
|
||||||
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
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 {
|
} 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
|
// 2. only support Github style pull request
|
||||||
if pull.Flow == issues_model.PullRequestFlowAGit {
|
if pull.Flow == issues_model.PullRequestFlowAGit {
|
||||||
return false, false, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. check user push permission on head repository
|
// 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 {
|
if err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. if the pull creator allows maintainer to edit, we need to check whether
|
// 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
|
// 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)
|
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
userAllowedToMergePR, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
|
userAllowedToMergePR, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
if userAllowedToMergePR {
|
if userAllowedToMergePR {
|
||||||
// the user is maintainer (can merge PR), and this PR is allowed to be edited by maintainers,
|
// 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
|
// then the user should inherit the PR poster's push/rebase permission for the head branch
|
||||||
if err := pull.LoadIssue(ctx); err != nil {
|
if err := pull.LoadIssue(ctx); err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
if err := pull.Issue.LoadPoster(ctx); err != nil {
|
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)
|
posterPushAllowed, posterRebaseAllowed, err := isUserAllowedToPushOrForcePushInRepoBranch(ctx, pull.Issue.Poster, pull.HeadRepo, pull.HeadBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
return ret, err
|
||||||
}
|
}
|
||||||
if !pushAllowed {
|
if !ret.MergeAllowed {
|
||||||
pushAllowed = posterPushAllowed
|
ret.MergeAllowed = posterPushAllowed
|
||||||
}
|
}
|
||||||
if !rebaseAllowed {
|
if !ret.RebaseAllowed {
|
||||||
rebaseAllowed = posterRebaseAllowed
|
ret.RebaseAllowed = posterRebaseAllowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. check base repository's AllowRebaseUpdate configuration
|
// 5. apply base repository's update configuration; it is a config on the base repo,
|
||||||
// it is a config in base repo but controls the head (fork) repo's "Update" behavior
|
// but it controls the head (fork) repo's "Update" behavior.
|
||||||
return pushAllowed, rebaseAllowed && prBaseUnit.PullRequestsConfig().AllowRebaseUpdate, nil
|
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 {
|
func syncCommitDivergence(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package pull
|
package pull
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@@ -23,11 +24,21 @@ import (
|
|||||||
func TestIsUserAllowedToUpdate(t *testing.T) {
|
func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
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 := 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))
|
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})
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
@@ -43,21 +54,33 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer db.DeleteByBean(t.Context(), protectedBranch)
|
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.NoError(t, err)
|
||||||
assert.False(t, pushAllowed)
|
assert.False(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
assert.False(t, rebaseAllowed)
|
||||||
|
assert.Equal(t, repo_model.UpdateStyleMerge, defaultMergeStyle)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("DisallowRebaseWhenConfigDisabled", func(t *testing.T) {
|
t.Run("DisallowRebaseWhenConfigDisabled", func(t *testing.T) {
|
||||||
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
setRepoAllowRebaseUpdate(t, pr2.BaseRepoID, false)
|
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.NoError(t, err)
|
||||||
assert.True(t, pushAllowed)
|
assert.True(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
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) {
|
t.Run("ReadOnlyAccessDenied", func(t *testing.T) {
|
||||||
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
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()))
|
require.NoError(t, pr2.LoadHeadRepo(t.Context()))
|
||||||
assert.NoError(t, access_model.RecalculateUserAccess(t.Context(), pr2.HeadRepo, user4.ID))
|
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.NoError(t, err)
|
||||||
assert.False(t, pushAllowed)
|
assert.False(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
assert.False(t, rebaseAllowed)
|
||||||
@@ -91,7 +114,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer db.DeleteByBean(t.Context(), protectedBranch)
|
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.NoError(t, err)
|
||||||
assert.True(t, pushAllowed)
|
assert.True(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
assert.False(t, rebaseAllowed)
|
||||||
@@ -102,7 +125,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
|||||||
t.Run("MaintainerEditRespectsPosterPermissions", func(t *testing.T) {
|
t.Run("MaintainerEditRespectsPosterPermissions", func(t *testing.T) {
|
||||||
pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||||
pr3.AllowMaintainerEdit = true
|
pr3.AllowMaintainerEdit = true
|
||||||
pushAllowed, rebaseAllowed, err := IsUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
pushAllowed, rebaseAllowed, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, pushAllowed)
|
assert.False(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
assert.False(t, rebaseAllowed)
|
||||||
@@ -132,7 +155,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
|||||||
require.NoError(t, pr3.LoadHeadRepo(t.Context()))
|
require.NoError(t, pr3.LoadHeadRepo(t.Context()))
|
||||||
assert.NoError(t, access_model.RecalculateUserAccess(t.Context(), pr3.HeadRepo, pr3Poster.ID))
|
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.NoError(t, err)
|
||||||
assert.True(t, pushAllowed)
|
assert.True(t, pushAllowed)
|
||||||
assert.True(t, rebaseAllowed)
|
assert.True(t, rebaseAllowed)
|
||||||
@@ -164,7 +187,7 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
|||||||
|
|
||||||
setRepoAllowRebaseUpdate(t, pr3.BaseRepoID, false)
|
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.NoError(t, err)
|
||||||
assert.True(t, pushAllowed)
|
assert.True(t, pushAllowed)
|
||||||
assert.False(t, rebaseAllowed)
|
assert.False(t, rebaseAllowed)
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{{$data := $.MergeBoxData}}
|
{{$data := $.MergeBoxData}}
|
||||||
{{$issueLink := $.IssueLink}}
|
|
||||||
<div class="tw-w-full flex-left-right">
|
<div class="tw-w-full flex-left-right">
|
||||||
<div class="flex-text-block">
|
<div class="flex-text-block">
|
||||||
{{svg "octicon-alert"}}
|
{{svg "octicon-alert"}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}}
|
{{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}}
|
||||||
</div>
|
</div>
|
||||||
{{if $data.UpdateAllowed}}
|
{{if $data.UpdatePrimaryAction}}
|
||||||
<div class="ui buttons" data-global-init="initRepoPullRequestUpdate">
|
<div class="ui buttons"{{if $data.UpdateStyleOptions}} data-global-init="initRepoPullRequestUpdate"{{end}}>
|
||||||
<button class="ui button link-action" data-url="{{$issueLink}}/update">
|
<button class="ui button link-action" data-url="{{$data.UpdatePrimaryAction.URL}}">
|
||||||
{{ctx.Locale.Tr "repo.pulls.update_branch"}}
|
{{$data.UpdatePrimaryAction.Text}}
|
||||||
</button>
|
</button>
|
||||||
{{if $data.UpdateByRebaseAllowed}}
|
{{if gt (len $data.UpdateStyleOptions) 1}}
|
||||||
<div class="ui dropdown icon button">
|
<div class="ui dropdown icon button">
|
||||||
{{svg "octicon-triangle-down"}}
|
{{svg "octicon-triangle-down"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item selected" data-update-url="{{$issueLink}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a>
|
{{range $data.UpdateStyleOptions}}
|
||||||
<a class="item" data-update-url="{{$issueLink}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
|
<a class="item {{if .Selected}}selected{{end}}" data-update-url="{{.URL}}">{{.Text}}</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -617,10 +617,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="pulls_allow_rebase_update" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowRebaseUpdate)}}checked{{end}}>
|
<input name="pulls_allow_merge_update" type="checkbox" {{if $prUnit.PullRequestsConfig.AllowMergeUpdate}}checked{{end}}>
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_merge_update"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="pulls_allow_rebase_update" type="checkbox" {{if $prUnit.PullRequestsConfig.AllowRebaseUpdate}}checked{{end}}>
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.pulls.allow_rebase_update"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.pulls.default_update_style"}}</label>
|
||||||
|
<div class="ui dropdown selection">
|
||||||
|
<select name="pulls_default_update_style">
|
||||||
|
{{range .PullsDefaultUpdateStyleOptions}}
|
||||||
|
<option value="{{.Value}}" {{if .Selected}}selected{{end}}>{{.Text}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="default text">
|
||||||
|
{{.PullsDefaultUpdateStyleText}}
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
{{range .PullsDefaultUpdateStyleOptions}}
|
||||||
|
<div class="item" data-value="{{.Value}}">{{.Text}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
|
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
|
||||||
|
|||||||
Generated
+18
@@ -25451,6 +25451,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowMerge"
|
"x-go-name": "AllowMerge"
|
||||||
},
|
},
|
||||||
|
"allow_merge_update": {
|
||||||
|
"description": "either `true` to allow updating pull request branch by merge, or `false` to prevent it.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AllowMergeUpdate"
|
||||||
|
},
|
||||||
"allow_rebase": {
|
"allow_rebase": {
|
||||||
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -25501,6 +25506,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "DefaultMergeStyle"
|
"x-go-name": "DefaultMergeStyle"
|
||||||
},
|
},
|
||||||
|
"default_update_style": {
|
||||||
|
"description": "set to an update style to be used by this repository: \"merge\" or \"rebase\".",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DefaultUpdateStyle"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"description": "a short description of the repository.",
|
"description": "a short description of the repository.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -28926,6 +28936,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowMerge"
|
"x-go-name": "AllowMerge"
|
||||||
},
|
},
|
||||||
|
"allow_merge_update": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AllowMergeUpdate"
|
||||||
|
},
|
||||||
"allow_rebase": {
|
"allow_rebase": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowRebase"
|
"x-go-name": "AllowRebase"
|
||||||
@@ -28993,6 +29007,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "DefaultTargetBranch"
|
"x-go-name": "DefaultTargetBranch"
|
||||||
},
|
},
|
||||||
|
"default_update_style": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DefaultUpdateStyle"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Description"
|
"x-go-name": "Description"
|
||||||
|
|||||||
@@ -5619,6 +5619,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowMerge"
|
"x-go-name": "AllowMerge"
|
||||||
},
|
},
|
||||||
|
"allow_merge_update": {
|
||||||
|
"description": "either `true` to allow updating pull request branch by merge, or `false` to prevent it.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AllowMergeUpdate"
|
||||||
|
},
|
||||||
"allow_rebase": {
|
"allow_rebase": {
|
||||||
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -5669,6 +5674,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "DefaultMergeStyle"
|
"x-go-name": "DefaultMergeStyle"
|
||||||
},
|
},
|
||||||
|
"default_update_style": {
|
||||||
|
"description": "set to an update style to be used by this repository: \"merge\" or \"rebase\".",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DefaultUpdateStyle"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"description": "a short description of the repository.",
|
"description": "a short description of the repository.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -9115,6 +9125,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowMerge"
|
"x-go-name": "AllowMerge"
|
||||||
},
|
},
|
||||||
|
"allow_merge_update": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "AllowMergeUpdate"
|
||||||
|
},
|
||||||
"allow_rebase": {
|
"allow_rebase": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "AllowRebase"
|
"x-go-name": "AllowRebase"
|
||||||
@@ -9184,6 +9198,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "DefaultTargetBranch"
|
"x-go-name": "DefaultTargetBranch"
|
||||||
},
|
},
|
||||||
|
"default_update_style": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DefaultUpdateStyle"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Description"
|
"x-go-name": "Description"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -69,6 +70,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
|||||||
allowRebaseMerge := false
|
allowRebaseMerge := false
|
||||||
allowSquash := false
|
allowSquash := false
|
||||||
allowFastForwardOnly := false
|
allowFastForwardOnly := false
|
||||||
|
allowMergeUpdate := false
|
||||||
|
allowRebaseUpdate := false
|
||||||
|
defaultUpdateStyle := string(repo_model.UpdateStyleMerge)
|
||||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
||||||
config := unit.PullRequestsConfig()
|
config := unit.PullRequestsConfig()
|
||||||
hasPullRequests = true
|
hasPullRequests = true
|
||||||
@@ -78,6 +82,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
|||||||
allowRebaseMerge = config.AllowRebaseMerge
|
allowRebaseMerge = config.AllowRebaseMerge
|
||||||
allowSquash = config.AllowSquash
|
allowSquash = config.AllowSquash
|
||||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||||
|
allowMergeUpdate = config.AllowMergeUpdate
|
||||||
|
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||||
|
defaultUpdateStyle = string(config.DefaultUpdateStyle)
|
||||||
}
|
}
|
||||||
archived := repo.IsArchived
|
archived := repo.IsArchived
|
||||||
hasProjects := false
|
hasProjects := false
|
||||||
@@ -122,6 +129,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
|||||||
AllowRebaseMerge: &allowRebaseMerge,
|
AllowRebaseMerge: &allowRebaseMerge,
|
||||||
AllowSquash: &allowSquash,
|
AllowSquash: &allowSquash,
|
||||||
AllowFastForwardOnly: &allowFastForwardOnly,
|
AllowFastForwardOnly: &allowFastForwardOnly,
|
||||||
|
AllowMergeUpdate: &allowMergeUpdate,
|
||||||
|
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||||
|
DefaultUpdateStyle: &defaultUpdateStyle,
|
||||||
Archived: &archived,
|
Archived: &archived,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,6 +158,9 @@ func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
|
|||||||
allowRebase := !*opts.AllowRebase
|
allowRebase := !*opts.AllowRebase
|
||||||
allowRebaseMerge := !*opts.AllowRebaseMerge
|
allowRebaseMerge := !*opts.AllowRebaseMerge
|
||||||
allowSquash := !*opts.AllowSquash
|
allowSquash := !*opts.AllowSquash
|
||||||
|
allowMergeUpdate := false
|
||||||
|
allowRebaseUpdate := true
|
||||||
|
defaultUpdateStyle := string(repo_model.UpdateStyleRebase)
|
||||||
archived := !*opts.Archived
|
archived := !*opts.Archived
|
||||||
|
|
||||||
return &api.EditRepoOption{
|
return &api.EditRepoOption{
|
||||||
@@ -169,6 +182,9 @@ func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
|
|||||||
AllowRebase: &allowRebase,
|
AllowRebase: &allowRebase,
|
||||||
AllowRebaseMerge: &allowRebaseMerge,
|
AllowRebaseMerge: &allowRebaseMerge,
|
||||||
AllowSquash: &allowSquash,
|
AllowSquash: &allowSquash,
|
||||||
|
AllowMergeUpdate: &allowMergeUpdate,
|
||||||
|
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||||
|
DefaultUpdateStyle: &defaultUpdateStyle,
|
||||||
Archived: &archived,
|
Archived: &archived,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,3 +504,31 @@ func TestAPIRepoEdit(t *testing.T) {
|
|||||||
assert.Equal(t, token, password)
|
assert.Equal(t, token, password)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIRepoEditPullUpdateSettingsValidation(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
repoURL := fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name)
|
||||||
|
|
||||||
|
allowMergeUpdate := false
|
||||||
|
allowRebaseUpdate := false
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", repoURL, &api.EditRepoOption{
|
||||||
|
AllowMergeUpdate: &allowMergeUpdate,
|
||||||
|
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
|
||||||
|
allowRebaseUpdate = true
|
||||||
|
defaultUpdateStyle := string(repo_model.UpdateStyleMerge)
|
||||||
|
req = NewRequestWithJSON(t, "PATCH", repoURL, &api.EditRepoOption{
|
||||||
|
AllowMergeUpdate: &allowMergeUpdate,
|
||||||
|
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||||
|
DefaultUpdateStyle: &defaultUpdateStyle,
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,14 +61,34 @@ func TestAPIPullUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
|
func updateRepoPullRequestConfig(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
|
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
|
||||||
repoUnit.PullRequestsConfig().AllowRebaseUpdate = allow
|
update(repoUnit.PullRequestsConfig())
|
||||||
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
|
||||||
|
updateRepoPullRequestConfig(t, repoID, func(c *repo_model.PullRequestsConfig) { c.AllowRebaseUpdate = allow })
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupOutdatedPRWithConfig creates an outdated PR (user2 base, org26 fork), loads its
|
||||||
|
// base repo, head repo, and issue, then applies the supplied PullRequestsConfig mutation.
|
||||||
|
func setupOutdatedPRWithConfig(t *testing.T, mutate func(*repo_model.PullRequestsConfig)) *issues_model.PullRequest {
|
||||||
|
t.Helper()
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
||||||
|
pr := createOutdatedPR(t, user, org26)
|
||||||
|
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||||
|
require.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||||
|
require.NoError(t, pr.LoadIssue(t.Context()))
|
||||||
|
if mutate != nil {
|
||||||
|
updateRepoPullRequestConfig(t, pr.BaseRepo.ID, mutate)
|
||||||
|
}
|
||||||
|
return pr
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIPullUpdateByRebase(t *testing.T) {
|
func TestAPIPullUpdateByRebase(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||||
// Create PR to test
|
// Create PR to test
|
||||||
@@ -120,6 +140,46 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAPIPullUpdateStyleSettings first checks that a disabled explicit style is
|
||||||
|
// forbidden, then guards back-compat: even when the repo's DefaultUpdateStyle is
|
||||||
|
// rebase, an API call with no `style` parameter must still perform a merge
|
||||||
|
// update so existing API clients don't silently flip behavior.
|
||||||
|
func TestAPIPullUpdateStyleSettings(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||||
|
pr := setupOutdatedPRWithConfig(t, func(c *repo_model.PullRequestsConfig) {
|
||||||
|
c.AllowMergeUpdate = false
|
||||||
|
c.AllowRebaseUpdate = true
|
||||||
|
c.DefaultUpdateStyle = repo_model.UpdateStyleRebase
|
||||||
|
})
|
||||||
|
|
||||||
|
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||||
|
require.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite))
|
||||||
|
require.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite))
|
||||||
|
|
||||||
|
session := loginUser(t, "user40")
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=merge", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
|
||||||
|
updateRepoPullRequestConfig(t, pr.BaseRepo.ID, func(c *repo_model.PullRequestsConfig) {
|
||||||
|
c.AllowMergeUpdate = true
|
||||||
|
})
|
||||||
|
|
||||||
|
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// merge update produces a merge commit on top of the head commit (Ahead=2),
|
||||||
|
// rebase update would replay the head commit alone (Ahead=1).
|
||||||
|
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, diffCount.Behind)
|
||||||
|
assert.Equal(t, 2, diffCount.Ahead)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
|
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
|
||||||
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
|
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
|
||||||
Name: "repo-pr-update",
|
Name: "repo-pr-update",
|
||||||
|
|||||||
Reference in New Issue
Block a user