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:
Nicolas
2026-05-16 10:06:40 +00:00
committed by GitHub
co-authored by OpenAI Codex silverwind Claude wxiaoguang
parent 16189a68c4
commit 34fd3c9f06
20 changed files with 493 additions and 60 deletions
+41 -4
View File
@@ -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 {
+12 -7
View File
@@ -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
}
+48 -2
View File
@@ -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)
}