mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-18 03:38:01 +02: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:
parent
16189a68c4
commit
34fd3c9f06
@ -29,6 +29,16 @@ const (
|
||||
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
|
||||
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
|
||||
_, 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/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -30,3 +31,82 @@ func TestDefaultTargetBranchSelection(t *testing.T) {
|
||||
repo.Units = nil
|
||||
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
|
||||
AllowManualMerge bool
|
||||
AutodetectManualMerge bool
|
||||
AllowMergeUpdate bool
|
||||
AllowRebaseUpdate bool
|
||||
DefaultUpdateStyle UpdateStyle
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultMergeStyle MergeStyle
|
||||
DefaultAllowMaintainerEdit bool
|
||||
@ -139,7 +141,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
AllowRebaseMerge: true,
|
||||
AllowSquash: true,
|
||||
AllowFastForwardOnly: true,
|
||||
AllowMergeUpdate: true,
|
||||
AllowRebaseUpdate: true,
|
||||
DefaultUpdateStyle: UpdateStyleMerge,
|
||||
DefaultAllowMaintainerEdit: true,
|
||||
}
|
||||
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
||||
@ -152,7 +156,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||
// set default values for existing PullRequestConfig in DB
|
||||
*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.
|
||||
@ -170,6 +176,32 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
|
||||
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 {
|
||||
return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
|
||||
}
|
||||
|
||||
@ -113,11 +113,13 @@ type Repository struct {
|
||||
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
|
||||
AllowSquash bool `json:"allow_squash_merge"`
|
||||
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
|
||||
AllowMergeUpdate bool `json:"allow_merge_update"`
|
||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||
AllowManualMerge bool `json:"allow_manual_merge"`
|
||||
AutodetectManualMerge bool `json:"autodetect_manual_merge"`
|
||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||
DefaultMergeStyle string `json:"default_merge_style"`
|
||||
DefaultUpdateStyle string `json:"default_update_style"`
|
||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Internal bool `json:"internal"`
|
||||
@ -224,12 +226,16 @@ type EditRepoOption struct {
|
||||
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.
|
||||
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.
|
||||
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
|
||||
// set to `true` to delete pr branch after merge by default
|
||||
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".
|
||||
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
|
||||
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
||||
// set to `true` to archive this repository.
|
||||
|
||||
@ -2138,7 +2138,9 @@
|
||||
"repo.settings.pulls_desc": "Enable Repository Pull Requests",
|
||||
"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.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.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": "Default branch (%s)",
|
||||
"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
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
|
||||
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
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.AllowManualMerge, opts.AllowManualMerge)
|
||||
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.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
|
||||
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
||||
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 {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
|
||||
@ -867,10 +867,8 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
|
||||
|
||||
if !prInfo.IsPullRequestBroken {
|
||||
data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken
|
||||
var err error
|
||||
data.UpdateAllowed, data.UpdateByRebaseAllowed, err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsUserAllowedToUpdate", err)
|
||||
prInfo.preparePullUpdateActions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -975,6 +973,45 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
|
||||
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) {
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch)
|
||||
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
|
||||
allowMerge bool // doer has permission to merge
|
||||
|
||||
ShowUpdatePullInfo bool
|
||||
UpdateAllowed bool
|
||||
UpdateByRebaseAllowed bool
|
||||
ShowUpdatePullInfo bool
|
||||
UpdatePrimaryAction *pullUpdateAction
|
||||
UpdateStyleOptions []*pullUpdateAction
|
||||
|
||||
MergeFormProps map[string]any
|
||||
ShowPullCommands bool
|
||||
@ -308,6 +308,12 @@ type pullMergeBoxData struct {
|
||||
InfoSections []*pullInfoSection
|
||||
}
|
||||
|
||||
type pullUpdateAction struct {
|
||||
URL string
|
||||
Text template.HTML
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// pullRequestViewInfo is a structured type for viewing pull request
|
||||
// Refactoring plan:
|
||||
// * move dynamic template-data-based variable into this struct
|
||||
@ -957,8 +963,6 @@ func UpdatePullRequest(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
rebase := ctx.FormString("style") == "rebase"
|
||||
|
||||
if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("LoadBaseRepo", err)
|
||||
return
|
||||
@ -968,13 +972,14 @@ func UpdatePullRequest(ctx *context.Context) {
|
||||
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 {
|
||||
ctx.ServerError("IsUserAllowedToMerge", err)
|
||||
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"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -49,6 +50,12 @@ const (
|
||||
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
|
||||
// settings template.
|
||||
func SettingsCtxData(ctx *context.Context) {
|
||||
@ -66,6 +73,7 @@ func SettingsCtxData(ctx *context.Context) {
|
||||
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
preparePullRequestSettings(ctx)
|
||||
|
||||
if ctx.Doer.IsAdmin {
|
||||
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
|
||||
func Settings(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||
@ -616,7 +653,8 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}
|
||||
|
||||
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,
|
||||
AllowMerge: form.PullsAllowMerge,
|
||||
AllowRebase: form.PullsAllowRebase,
|
||||
@ -625,12 +663,20 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
|
||||
AllowManualMerge: form.PullsAllowManualMerge,
|
||||
AutodetectManualMerge: form.EnableAutodetectManualMerge,
|
||||
AllowMergeUpdate: form.PullsAllowMergeUpdate,
|
||||
AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
|
||||
DefaultUpdateStyle: defaultUpdateStyle,
|
||||
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
||||
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
||||
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() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
{{$data := $.MergeBoxData}}
|
||||
{{$issueLink := $.IssueLink}}
|
||||
<div class="tw-w-full flex-left-right">
|
||||
<div class="flex-text-block">
|
||||
{{svg "octicon-alert"}}
|
||||
{{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}}
|
||||
</div>
|
||||
{{if $data.UpdateAllowed}}
|
||||
<div class="ui buttons" data-global-init="initRepoPullRequestUpdate">
|
||||
<button class="ui button link-action" data-url="{{$issueLink}}/update">
|
||||
{{ctx.Locale.Tr "repo.pulls.update_branch"}}
|
||||
{{if $data.UpdatePrimaryAction}}
|
||||
<div class="ui buttons"{{if $data.UpdateStyleOptions}} data-global-init="initRepoPullRequestUpdate"{{end}}>
|
||||
<button class="ui button link-action" data-url="{{$data.UpdatePrimaryAction.URL}}">
|
||||
{{$data.UpdatePrimaryAction.Text}}
|
||||
</button>
|
||||
{{if $data.UpdateByRebaseAllowed}}
|
||||
{{if gt (len $data.UpdateStyleOptions) 1}}
|
||||
<div class="ui dropdown icon button">
|
||||
{{svg "octicon-triangle-down"}}
|
||||
<div class="menu">
|
||||
<a class="item selected" data-update-url="{{$issueLink}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a>
|
||||
<a class="item" data-update-url="{{$issueLink}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
|
||||
{{range $data.UpdateStyleOptions}}
|
||||
<a class="item {{if .Selected}}selected{{end}}" data-update-url="{{.URL}}">{{.Text}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@ -617,10 +617,34 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<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>
|
||||
</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="ui checkbox">
|
||||
<input name="default_delete_branch_after_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge)}}checked{{end}}>
|
||||
|
||||
18
templates/swagger/v1_json.tmpl
generated
18
templates/swagger/v1_json.tmpl
generated
@ -25451,6 +25451,11 @@
|
||||
"type": "boolean",
|
||||
"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": {
|
||||
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
||||
"type": "boolean",
|
||||
@ -25501,6 +25506,11 @@
|
||||
"type": "string",
|
||||
"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": "a short description of the repository.",
|
||||
"type": "string",
|
||||
@ -28926,6 +28936,10 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowMerge"
|
||||
},
|
||||
"allow_merge_update": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowMergeUpdate"
|
||||
},
|
||||
"allow_rebase": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowRebase"
|
||||
@ -28993,6 +29007,10 @@
|
||||
"type": "string",
|
||||
"x-go-name": "DefaultTargetBranch"
|
||||
},
|
||||
"default_update_style": {
|
||||
"type": "string",
|
||||
"x-go-name": "DefaultUpdateStyle"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
|
||||
@ -5619,6 +5619,11 @@
|
||||
"type": "boolean",
|
||||
"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": {
|
||||
"description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
|
||||
"type": "boolean",
|
||||
@ -5669,6 +5674,11 @@
|
||||
"type": "string",
|
||||
"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": "a short description of the repository.",
|
||||
"type": "string",
|
||||
@ -9115,6 +9125,10 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowMerge"
|
||||
},
|
||||
"allow_merge_update": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowMergeUpdate"
|
||||
},
|
||||
"allow_rebase": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "AllowRebase"
|
||||
@ -9184,6 +9198,10 @@
|
||||
"type": "string",
|
||||
"x-go-name": "DefaultTargetBranch"
|
||||
},
|
||||
"default_update_style": {
|
||||
"type": "string",
|
||||
"x-go-name": "DefaultUpdateStyle"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -69,6 +70,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
||||
allowRebaseMerge := false
|
||||
allowSquash := false
|
||||
allowFastForwardOnly := false
|
||||
allowMergeUpdate := false
|
||||
allowRebaseUpdate := false
|
||||
defaultUpdateStyle := string(repo_model.UpdateStyleMerge)
|
||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
hasPullRequests = true
|
||||
@ -78,6 +82,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
||||
allowRebaseMerge = config.AllowRebaseMerge
|
||||
allowSquash = config.AllowSquash
|
||||
allowFastForwardOnly = config.AllowFastForwardOnly
|
||||
allowMergeUpdate = config.AllowMergeUpdate
|
||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||
defaultUpdateStyle = string(config.DefaultUpdateStyle)
|
||||
}
|
||||
archived := repo.IsArchived
|
||||
hasProjects := false
|
||||
@ -122,6 +129,9 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
|
||||
AllowRebaseMerge: &allowRebaseMerge,
|
||||
AllowSquash: &allowSquash,
|
||||
AllowFastForwardOnly: &allowFastForwardOnly,
|
||||
AllowMergeUpdate: &allowMergeUpdate,
|
||||
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||
DefaultUpdateStyle: &defaultUpdateStyle,
|
||||
Archived: &archived,
|
||||
}
|
||||
}
|
||||
@ -148,6 +158,9 @@ func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
|
||||
allowRebase := !*opts.AllowRebase
|
||||
allowRebaseMerge := !*opts.AllowRebaseMerge
|
||||
allowSquash := !*opts.AllowSquash
|
||||
allowMergeUpdate := false
|
||||
allowRebaseUpdate := true
|
||||
defaultUpdateStyle := string(repo_model.UpdateStyleRebase)
|
||||
archived := !*opts.Archived
|
||||
|
||||
return &api.EditRepoOption{
|
||||
@ -169,6 +182,9 @@ func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
|
||||
AllowRebase: &allowRebase,
|
||||
AllowRebaseMerge: &allowRebaseMerge,
|
||||
AllowSquash: &allowSquash,
|
||||
AllowMergeUpdate: &allowMergeUpdate,
|
||||
AllowRebaseUpdate: &allowRebaseUpdate,
|
||||
DefaultUpdateStyle: &defaultUpdateStyle,
|
||||
Archived: &archived,
|
||||
}
|
||||
}
|
||||
@ -488,3 +504,31 @@ func TestAPIRepoEdit(t *testing.T) {
|
||||
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()
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
// 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 {
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
|
||||
Name: "repo-pr-update",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user