Files
gitea/tests/integration/pull_update_test.go
Nicolas 34fd3c9f06 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>
2026-05-16 10:06:40 +00:00

284 lines
11 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"net/url"
"strings"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIPullUpdate(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// Create PR to test
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.LoadIssue(t.Context()))
// Test GetDiverging
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
require.NoError(t, err)
assert.Equal(t, 1, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)
assert.Equal(t, diffCount.Behind, pr.CommitsBehind)
assert.Equal(t, diffCount.Ahead, pr.CommitsAhead)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
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)
// Test GetDiverging after update
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)
assert.Eventually(t, func() bool {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
return diffCount.Behind == pr.CommitsBehind && diffCount.Ahead == pr.CommitsAhead
}, 5*time.Second, 20*time.Millisecond)
})
}
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})
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
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
pr := createOutdatedPR(t, user, org26)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
// Test GetDiverging
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
assert.NoError(t, err)
assert.Equal(t, 1, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)
assert.NoError(t, pr.LoadIssue(t.Context()))
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
// use a user which have write access to the pr but not write permission to the head repository to do the rebase
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
token40 := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteRepository)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusForbidden)
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusOK)
// Test GetDiverging after update
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
assert.NoError(t, err)
assert.Equal(t, 0, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)
})
}
// 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",
Description: "repo-tmp-pr-update description",
AutoInit: true,
Gitignores: "C,C++",
License: "MIT",
Readme: "Default",
IsPrivate: false,
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
headRepo, err := repo_service.ForkRepository(t.Context(), actor, forkOrg, repo_service.ForkRepoOptions{
BaseRepo: baseRepo,
Name: "repo-pr-update",
Description: "desc",
})
assert.NoError(t, err)
assert.NotEmpty(t, headRepo)
// create a commit on base Repo
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, actor, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "File_A",
ContentReader: strings.NewReader("File A"),
},
},
Message: "Add File A",
OldBranch: "master",
NewBranch: "master",
Author: &files_service.IdentityOptions{
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
// create a commit on head Repo
_, err = files_service.ChangeRepoFiles(t.Context(), headRepo, actor, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "File_B",
ContentReader: strings.NewReader("File B"),
},
},
Message: "Add File on PR branch",
OldBranch: "master",
NewBranch: "newBranch",
Author: &files_service.IdentityOptions{
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: actor.Name,
GitUserEmail: actor.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
// create Pull
pullIssue := &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "Test Pull -to-update-",
PosterID: actor.ID,
Poster: actor,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: headRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "newBranch",
BaseBranch: "master",
HeadRepo: headRepo,
BaseRepo: baseRepo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
return issue.PullRequest
}