mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 14:03:24 +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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user