feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
+81 -53
View File
@@ -11,41 +11,42 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
"gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/process"
"gitea.dev/modules/queue"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/automergequeue"
notify_service "gitea.dev/services/notify"
)
// prPatchCheckerQueue represents a queue to handle update pull request tests
var prPatchCheckerQueue *queue.WorkerPoolQueue[string]
var (
ErrIsClosed = errors.New("pull is closed")
ErrNoPermissionToMerge = errors.New("no permission to merge")
ErrNotReadyToMerge = errors.New("not ready to merge")
ErrHasMerged = errors.New("has already been merged")
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
ErrNotMergeableState = errors.New("not in mergeable state")
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
ErrIsClosed = errors.New("pull is closed")
ErrNoPermissionToMerge = errors.New("no permission to merge")
ErrNotReadyToMerge = errors.New("not ready to merge")
ErrHasMerged = errors.New("has already been merged")
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
ErrNotMergeableState = errors.New("not in mergeable state")
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
ErrHeadCommitsNotAllVerified = errors.New("the branch requires signed commits but not all head commits are verified")
)
func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool {
@@ -132,7 +133,13 @@ const (
)
// CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminForceMerge bool) error {
// mergeStyle tailors the "require signed commits" prechecks:
// - fast-forward-only: no Gitea commit is produced, so Gitea's merge-signing check is skipped;
// only the user's head commits are verified.
// - merge: both the head commits must be verified and Gitea must sign the merge commit.
// - rebase, rebase-merge, squash: Gitea rewrites the commits and signs each, so only Gitea's
// signing ability is checked.
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, mergeStyle repo_model.MergeStyle, forceMerge bool) error {
return db.WithTx(stdCtx, func(ctx context.Context) error {
if pr.HasMerged {
return ErrHasMerged
@@ -161,7 +168,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
return ErrIsWorkInProgress
}
if !pr.CanAutoMerge() && !pr.IsEmpty() {
if !pr.IsStatusMergeable() && !pr.IsEmpty() {
return ErrNotMergeableState
}
@@ -169,21 +176,21 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
return ErrIsChecking
}
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
if !errors.Is(err, ErrNotReadyToMerge) {
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
return err
if errProtection := CheckPullBranchProtections(ctx, pr, false); errProtection != nil {
if !errors.Is(errProtection, ErrNotReadyToMerge) {
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, errProtection)
return errProtection
}
// Now the branch protection check failed, check whether the failure could be skipped (skip by setting err = nil)
// * when doing Auto Merge (Scheduled Merge After Checks Succeed), skip the branch protection check
if mergeCheckType == MergeCheckTypeAuto {
err = nil
errProtection = nil
}
// * if admin tries to "Force Merge", they could sometimes skip the branch protection check
if adminForceMerge {
// * if the doer tries to "Force Merge", check whether it is really allowed
if forceMerge {
isRepoAdmin, errForceMerge := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer)
if errForceMerge != nil {
return fmt.Errorf("IsUserRepoAdmin failed, repo: %v, doer: %v, err: %w", pr.BaseRepoID, doer.ID, errForceMerge)
@@ -194,20 +201,22 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
return fmt.Errorf("GetFirstMatchProtectedBranchRule failed, repo: %v, base branch: %v, err: %w", pr.BaseRepoID, pr.BaseBranch, errForceMerge)
}
// if doer is admin and the "Force Merge" is not blocked, then clear the branch protection check error
blockAdminForceMerge := protectedBranchRule != nil && protectedBranchRule.BlockAdminMergeOverride
if isRepoAdmin && !blockAdminForceMerge {
err = nil
canForceMerge := isRepoAdmin
if protectedBranchRule != nil {
canForceMerge = git_model.CanBypassBranchProtection(ctx, protectedBranchRule, doer, isRepoAdmin)
}
if canForceMerge {
errProtection = nil
}
}
// If there is still a branch protection check error, return it
if err != nil {
return err
if errProtection != nil {
return errProtection
}
}
if _, err := isSignedIfRequired(ctx, pr, doer); err != nil {
if err := checkSigningRequirements(ctx, pr, doer, mergeStyle); err != nil {
return err
}
@@ -221,26 +230,45 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
})
}
// isSignedIfRequired check if merge will be signed if required
func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
// checkSigningRequirements enforces the target branch's RequireSignedCommits rule
// against the selected merge style:
// - fast-forward-only and merge keep the user's commits on the base branch, so
// those commits must all be verified, or the pre-receive hook will reject the
// push with a generic error.
// - fast-forward-only creates no Gitea commit, so Gitea's signing key is not used.
// - merge, rebase, rebase-merge and squash produce a Gitea-signed commit, so
// Gitea must be configured to sign it.
func checkSigningRequirements(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle) error {
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, err
return err
}
if pb == nil || !pb.RequireSignedCommits {
return true, nil
return nil
}
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
return false, err
return err
}
defer closer.Close()
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo)
if mergeStyle == repo_model.MergeStyleFastForwardOnly || mergeStyle == repo_model.MergeStyleMerge {
verified, err := asymkey_service.AllHeadCommitsVerified(ctx, pr, gitRepo)
if err != nil {
return err
}
if !verified {
return ErrHeadCommitsNotAllVerified
}
}
return sign, err
if mergeStyle != repo_model.MergeStyleFastForwardOnly {
if _, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName()); err != nil {
return err
}
}
return nil
}
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
@@ -8,17 +8,18 @@ import (
"testing"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/automergequeue"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/graceful"
"gitea.dev/modules/queue"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/services/automergequeue"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -73,6 +74,39 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
prPatchCheckerQueue = nil
}
func TestCheckSigningRequirementsHeadCommits(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.NoError(t, pr.LoadBaseRepo(ctx))
require.NoError(t, pr.LoadHeadRepo(ctx))
check := func() error {
return checkSigningRequirements(ctx, pr, nil, repo_model.MergeStyleFastForwardOnly)
}
// No protected branch rule on the base branch: the check must pass.
require.NoError(t, check())
// Protected branch without RequireSignedCommits: the check must still pass.
require.NoError(t, git_model.UpdateProtectBranch(ctx, pr.BaseRepo, &git_model.ProtectedBranch{
RepoID: pr.BaseRepoID,
RuleName: pr.BaseBranch,
RequireSignedCommits: false,
}, git_model.WhitelistOptions{}))
require.NoError(t, check())
// With RequireSignedCommits enabled: the test fixture commits have no signatures,
// so the check must report ErrHeadCommitsNotAllVerified.
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
require.NoError(t, err)
require.NotNil(t, pb)
pb.RequireSignedCommits = true
require.NoError(t, git_model.UpdateProtectBranch(ctx, pr.BaseRepo, pb, git_model.WhitelistOptions{}))
require.ErrorIs(t, check(), ErrHeadCommitsNotAllVerified)
}
func TestMarkPullRequestAsMergeable(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
@@ -7,14 +7,14 @@ import (
"context"
"slices"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
)
const maxPushCommitsInCommentCount = 1000
@@ -6,13 +6,13 @@ package pull
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -8,14 +8,18 @@ import (
"context"
"errors"
"fmt"
"slices"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/commitstatus"
"gitea.dev/modules/container"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/glob"
"gitea.dev/modules/log"
)
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
@@ -69,11 +73,25 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, fmt.Errorf("GetLatestCommitStatus: %w", err)
return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
}
if pb == nil || !pb.EnableStatusCheck {
if pb == nil {
return true, nil
}
if !pb.EnableStatusCheck {
// The branch's own status check is off, but required scoped checks (mandated by the owner or instance admin) still gate the merge.
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, err
}
required, err := EffectiveRequiredContexts(ctx, pr.BaseRepo, pb)
if err != nil {
return false, err
}
if len(required) == 0 {
// With none in effect there is nothing to enforce, so don't block
return true, nil
}
}
state, err := GetPullRequestCommitStatusState(ctx, pr)
if err != nil {
@@ -130,10 +148,54 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
if err != nil {
return "", fmt.Errorf("LoadProtectedBranch: %w", err)
}
var requiredContexts []string
if pb != nil {
requiredContexts = pb.StatusCheckContexts
requiredContexts, err := EffectiveRequiredContexts(ctx, pr.BaseRepo, pb)
if err != nil {
return "", err
}
return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil
}
// EffectiveRequiredContexts returns the required status-check contexts to enforce, drawn from:
// 1. every required scoped workflow's status-check patterns effective for the repo (always)
// 2. each given protected branch rule's own configured contexts, only when that rule's status check is enabled
//
// Passing no rule or a single nil rule yields nothing, not even scoped patterns.
// A single rule yields that rule's effective contexts.
// Passing several rules unions their effective contexts; this is used when the governing rule is not yet known.
func EffectiveRequiredContexts(ctx context.Context, repo *repo_model.Repository, pbs ...*git_model.ProtectedBranch) ([]string, error) {
// No protection rule in effect: nothing is required.
if len(pbs) == 0 || (len(pbs) == 1 && pbs[0] == nil) {
return nil, nil
}
sources, err := actions_model.GetEffectiveScopedWorkflowSources(ctx, repo.OwnerID)
if err != nil {
return nil, fmt.Errorf("GetEffectiveScopedWorkflowSources: %w", err)
}
required := make(container.Set[string])
// Every required scoped workflow's admin-authored status-check patterns, matched must-present-and-pass:
// a required scoped check that posts no matching status blocks the merge.
for _, source := range sources {
for _, cfg := range source.WorkflowConfigs {
if !cfg.Required {
continue
}
required.AddMultiple(cfg.Patterns...)
}
}
// Union the configured contexts of every rule whose own status check is enabled (a disabled rule contributes none)
for _, pb := range pbs {
if pb == nil || !pb.EnableStatusCheck {
continue
}
required.AddMultiple(pb.StatusCheckContexts...)
}
values := required.Values()
slices.Sort(values) // stable output
return values, nil
}
@@ -7,10 +7,15 @@ package pull
import (
"testing"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/commitstatus"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/commitstatus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMergeRequiredContextsCommitStatus(t *testing.T) {
@@ -90,3 +95,72 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i)
}
}
// TestEffectiveRequiredContexts: every required scoped workflow's stored status-check patterns are appended to the
// branch protection's configured contexts unconditionally (must-present; the matching is done downstream).
func TestEffectiveRequiredContexts(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
consumer := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // owned by user5
pbOn := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"configured/check"}}
t.Run("nil protected branch: nil", func(t *testing.T) {
got, err := EffectiveRequiredContexts(t.Context(), consumer, nil)
require.NoError(t, err)
assert.Nil(t, got)
})
t.Run("status checks disabled, no required scoped: nothing required", func(t *testing.T) {
pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}}
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff)
require.NoError(t, err)
assert.Empty(t, got) // the rule's own status check is off and no required scoped workflow applies -> nothing gates
})
t.Run("owner with no scoped sources: configured contexts unchanged", func(t *testing.T) {
noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999}
got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, pbOn)
require.NoError(t, err)
assert.Equal(t, []string{"configured/check"}, got)
})
t.Run("required workflow patterns appended", func(t *testing.T) {
require.NoError(t, db.Insert(t.Context(), &actions_model.ActionScopedWorkflowSource{
OwnerID: consumer.OwnerID,
SourceRepoID: 1,
WorkflowConfigs: map[string]*actions_model.ScopedWorkflowConfig{
"ci.yaml": {Required: true, Patterns: []string{"org/src: ci.yaml / build (pull_request)", "org/src: ci.yaml / lint (pull_request)"}},
"old.yaml": {Required: false, Patterns: []string{"org/src: old.yaml / *"}}, // kept as history, must NOT be enforced
},
}))
// No status is passed/needed: required patterns are enforced even though nothing has posted them yet (must-present).
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOn)
require.NoError(t, err)
assert.ElementsMatch(t, []string{
"configured/check",
"org/src: ci.yaml / build (pull_request)",
"org/src: ci.yaml / lint (pull_request)",
}, got)
assert.NotContains(t, got, "org/src: old.yaml / *", "a non-required (history) config must not be enforced")
})
t.Run("status checks disabled, with required scoped: only the scoped patterns gate", func(t *testing.T) {
pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}}
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff)
require.NoError(t, err)
// "configured/check" is dropped (the rule's own status check is off); only the required scoped patterns remain.
assert.ElementsMatch(t, []string{
"org/src: ci.yaml / build (pull_request)",
"org/src: ci.yaml / lint (pull_request)",
}, got)
})
t.Run("several rules union their enabled contexts, deduplicated; a disabled rule contributes none", func(t *testing.T) {
noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999} // no scoped sources, so only the rules' contexts gate
a := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"a/check", "shared/check"}}
b := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"b/check", "shared/check"}}
off := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"off/check"}}
got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, a, b, off)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"a/check", "b/check", "shared/check"}, got)
})
}
+4 -4
View File
@@ -8,10 +8,10 @@ import (
"context"
"errors"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
)
var ErrUserHasNoPermissionForAction = errors.New("user not allowed to do this action")
+7 -7
View File
@@ -11,13 +11,13 @@ import (
"io"
"strconv"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/git/pipeline"
"gitea.dev/modules/lfs"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
"golang.org/x/sync/errgroup"
)
@@ -7,9 +7,9 @@ package pull
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"gitea.dev/models/unittest"
_ "code.gitea.io/gitea/models/actions"
_ "gitea.dev/models/actions"
)
func TestMain(m *testing.M) {
+58 -48
View File
@@ -16,27 +16,28 @@ import (
"strings"
"unicode"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
pull_model "gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/references"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
issue_service "gitea.dev/services/issue"
notify_service "gitea.dev/services/notify"
)
// getMergeMessage composes the message used when merging a pull request.
@@ -218,9 +219,28 @@ func (err ErrInvalidMergeStyle) Unwrap() error {
return util.ErrInvalidArgument
}
func addTestPullRequestTaskAfterWebOperation(pr *issues_model.PullRequest, doer *user_model.User) {
// This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue).
// This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc)
// immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082.
// But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run.
// TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue.
go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: pr.BaseRepo.ID,
Doer: doer,
Branch: pr.BaseBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}
// Merge merges pull request to base repository.
// Caller should check PR is ready to be merged (review and status checks)
func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
func Merge(pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repo: %v", err)
return fmt.Errorf("unable to load base repo: %w", err)
@@ -241,37 +261,27 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
return ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
}
releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID))
if err != nil {
log.Error("lock.Lock(): %v", err)
return fmt.Errorf("lock.Lock: %w", err)
}
defer releaser()
defer func() {
// This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue).
// This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc)
// immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082.
// But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run.
// TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue.
go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: pr.BaseRepo.ID,
Doer: doer,
Branch: pr.BaseBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}()
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
releaser()
err = globallock.LockAndDo(ctx, getPullWorkingLockKey(pr.ID), func(ctx context.Context) error {
_, err := doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
return err
})
defer addTestPullRequestTaskAfterWebOperation(pr, doer) // keep the same behavior as old code: always call AddTestPullRequestTask
// TODO: the "merge" operation has finished, there could still be some edge cases:
// * if the post-process hook isn't executed correctly:
// * the commit has been merged into target branch
// * the PR's status is still "open (unmerged)"
// * something wrong happens (e.g.: out of sync?)
// * maybe this is the reason that why the duplicate AddTestPullRequestTask is called in defer func above
if err != nil {
return err
}
// TODO: it is questionable whether it should return error here, the "merge" operation has succeeded
return handleMergePostProcess(ctx, pr.ID, doer, wasAutoMerged)
}
func handleMergePostProcess(ctx context.Context, prID int64, doer *user_model.User, wasAutoMerged bool) error {
// reload pull request because it has been updated by post receive hook
pr, err = issues_model.GetPullRequestByID(ctx, pr.ID)
pr, err := issues_model.GetPullRequestByID(ctx, prID)
if err != nil {
return err
}
@@ -4,9 +4,9 @@
package pull
import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch)
@@ -4,9 +4,9 @@
package pull
import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch)
@@ -13,15 +13,14 @@ import (
"strings"
"time"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
asymkey_service "gitea.dev/services/asymkey"
)
type mergeContext struct {
@@ -103,15 +102,18 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
mergeCtx.sig = doer.NewGitSig()
mergeCtx.committer = mergeCtx.sig
gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
gitRepo, err := git.OpenRepository(ctx, mergeCtx.tmpBasePath)
if err != nil {
defer cancel()
return nil, nil, fmt.Errorf("failed to open temp git repo for pr[%d]: %w", mergeCtx.pr.ID, err)
}
defer gitRepo.Close()
// Determine if we should sign
sign, key, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, gitRepo)
// Determine if we should sign, using the temp repo's own refs (see SignMerge for why)
sign, key, signer, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, git.BranchPrefix+tmpRepoBaseBranch, git.BranchPrefix+tmpRepoTrackingBranch)
if err != nil && !asymkey_service.IsErrWontSign(err) {
log.Error("%-v SignMerge: %v", mergeCtx.pr, err) // the merge proceeds unsigned regardless, so log it here
}
if sign {
mergeCtx.signKey = key
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
@@ -7,11 +7,11 @@ import (
"fmt"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
)
// getRebaseAmendMessage composes the message to amend commits in rebase merge of a pull request.
@@ -6,13 +6,13 @@ package pull
import (
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// doMergeStyleSquash gets a commit author signature for squash commits
@@ -22,7 +22,7 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) {
return nil, err
}
// Try to get an signature from the same user in one of the commits, as the
// Try to get a signature from the same user in one of the commits, as the
// poster email might be private or commits might have a different signature
// than the primary email address of the poster.
gitRepo, err := git.OpenRepository(ctx, ctx.tmpBasePath)
@@ -32,9 +32,9 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) {
}
defer gitRepo.Close()
commits, err := gitRepo.CommitsBetweenIDs(tmpRepoTrackingBranch, "HEAD")
commits, err := gitRepo.CommitsBetween(git.RefNameFromBranch(tmpRepoTrackingBranch), git.RefNameHead, -1)
if err != nil {
log.Error("%-v Unable to get commits between: %s %s: %v", ctx.pr, "HEAD", tmpRepoTrackingBranch, err)
log.Error("%-v Unable to get commits between: head and tracking branch: %v", ctx.pr, err)
return nil, err
}
@@ -65,9 +65,7 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
}
if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
// add trailer
message = AddCommitMessageTailer(message, "Co-authored-by", sig.String())
message = AddCommitMessageTailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used
message = AddCommitMessageTailer(message, git.CoAuthoredByTrailer, sig.String())
}
cmdCommit := gitcmd.NewCommand("commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
@@ -8,11 +8,11 @@ import (
"errors"
"fmt"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
)
// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty
@@ -8,9 +8,9 @@ import (
"fmt"
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git/gitcmd"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unittest"
"gitea.dev/modules/git/gitcmd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+9 -9
View File
@@ -12,15 +12,15 @@ import (
"path/filepath"
"strings"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/glob"
"gitea.dev/modules/log"
"gitea.dev/modules/process"
"gitea.dev/modules/util"
)
// DownloadDiffOrPatch will write the patch for the pr to the writer
@@ -12,8 +12,8 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
)
// lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
@@ -6,8 +6,8 @@ package pull
import (
"context"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
git_model "gitea.dev/models/git"
repo_model "gitea.dev/models/repo"
)
func CreateOrUpdateProtectedBranch(ctx context.Context, repo *repo_model.Repository,
+137 -129
View File
@@ -4,37 +4,39 @@
package pull
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"regexp"
"slices"
"strings"
"time"
"unicode"
"unicode/utf8"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
git_service "code.gitea.io/gitea/services/git"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
git_service "gitea.dev/services/git"
issue_service "gitea.dev/services/issue"
notify_service "gitea.dev/services/notify"
)
func getPullWorkingLockKey(prID int64) string {
@@ -50,7 +52,7 @@ type NewPullRequestOptions struct {
AssigneeIDs []int64
Reviewers []*user_model.User
TeamReviewers []*organization.Team
ProjectID int64
ProjectIDs []int64
}
// NewPullRequest creates new pull request with labels for repository.
@@ -95,7 +97,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
}
assigneeCommentMap := make(map[int64]*issues_model.Comment)
assignees := make(map[int64]*user_model.User)
var reviewNotifiers []*issue_service.ReviewRequestNotifier
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
@@ -103,15 +105,21 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
}
for _, assigneeID := range assigneeIDs {
comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, false)
assignee, err := user_model.GetUserByID(ctx, assigneeID)
if err != nil {
log.Error("GetUserByID: %v", err)
continue
}
comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assignee)
if err != nil {
return err
}
assignees[assigneeID] = assignee
assigneeCommentMap[assigneeID] = comment
}
if opts.ProjectID > 0 && canAssignProject {
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, opts.ProjectID, 0); err != nil {
if len(opts.ProjectIDs) > 0 && canAssignProject {
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, opts.ProjectIDs); err != nil {
return err
}
}
@@ -185,12 +193,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
if issue.Milestone != nil {
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
}
for _, assigneeID := range assigneeIDs {
assignee, err := user_model.GetUserByID(ctx, assigneeID)
if err != nil {
return ErrDependenciesLeft
}
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
for _, assignee := range assignees {
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assignee.ID])
}
return nil
@@ -322,6 +326,14 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
return fmt.Errorf("syncCommitDivergence: %w", err)
}
// The "official" flag of existing reviews was computed against the previous
// target branch's protection rules, so re-evaluate it against the new branch.
// Otherwise a stale official approval could bypass the new branch's protection.
pr.Issue.PullRequest = pr
if err := issues_model.RecalculateReviewsOfficial(ctx, pr.Issue); err != nil {
return fmt.Errorf("RecalculateReviewsOfficial: %w", err)
}
// Create comment
options := &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeChangeTargetBranch,
@@ -478,7 +490,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
}
}
notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
notify_service.PullRequestSynchronized(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
}
}
}
@@ -764,116 +776,88 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
return errors.Join(errs...)
}
var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) (_ string, err error) {
if err := pr.LoadIssue(ctx); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
return "", err
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
return ""
return "", err
}
if pr.HeadRepo == nil {
var err error
pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
if err != nil {
log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
return ""
return "", err
}
}
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
if err != nil {
log.Error("Unable to open head repository: Error: %v", err)
return ""
return "", err
}
defer closer.Close()
var headCommit *git.Commit
var headCommitRef git.RefName
if pr.Flow == issues_model.PullRequestFlowGithub {
headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
headCommitRef = git.RefNameFromBranch(pr.HeadBranch)
} else {
pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
log.Error("Unable to get head commit: %s Error: %v", pr.GetGitHeadRefName(), err)
return ""
return "", err
}
headCommit, err = gitRepo.GetCommit(pr.HeadCommitID)
}
if err != nil {
log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
return ""
headCommitRef = git.RefNameFromCommit(pr.HeadCommitID)
}
mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
if err != nil {
log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
return ""
}
mergeBaseRef := git.RefNameFromCommit(pr.MergeBase)
limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
limitedCommits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit)
if err != nil {
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
return ""
return "", err
}
mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message
if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message
}
coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits)
return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil
}
func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string {
if len(coAuthors) == 0 {
return mergeMessage
}
msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage)
if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" {
msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace)
msgSep = "\n\n---------\n\n"
}
var sb strings.Builder
sb.WriteString(msgContent)
sb.WriteString(msgSep)
if msgTrailer = strings.TrimSpace(msgTrailer); msgTrailer != "" {
sb.WriteString(msgTrailer)
sb.WriteRune('\n')
}
for _, author := range coAuthors {
sb.WriteString(git.CoAuthoredByTrailer + ": ")
sb.WriteString(author)
sb.WriteRune('\n')
}
return sb.String()
}
func collectSquashMergeCommitCoAuthors(ctx context.Context, gitRepo *git.Repository, pr *issues_model.PullRequest, headCommitRef, mergeBaseRef git.RefName, limitFirst int, limitedCommits []*git.Commit) []string {
posterSig := pr.Issue.Poster.NewGitSig().String()
uniqueAuthors := make(container.Set[string])
authors := make([]string, 0, len(commits))
stringBuilder := strings.Builder{}
authors := make([]string, 0, len(limitedCommits))
if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
// use PR's title and description as squash commit message
message := strings.TrimSpace(pr.Issue.Content)
stringBuilder.WriteString(message)
if stringBuilder.Len() > 0 {
stringBuilder.WriteRune('\n')
if !commitMessageTrailersPattern.MatchString(message) {
// TODO: this trailer check doesn't work with the separator line added below for the co-authors
stringBuilder.WriteRune('\n')
}
}
} else {
// use PR's commit messages as squash commit message
// commits list is in reverse chronological order
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
for i := len(commits) - 1; i >= 0; i-- {
commit := commits[i]
msg := strings.TrimSpace(commit.Message())
if msg == "" {
continue
}
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg)
if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize {
tmp := stringBuilder.String()
wasValidUtf8 := utf8.ValidString(tmp)
tmp = tmp[:maxMsgSize] + "..."
if wasValidUtf8 {
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
tmp = strings.ToValidUTF8(tmp, "")
}
stringBuilder.Reset()
stringBuilder.WriteString(tmp)
break
}
}
}
// collect co-authors
for _, commit := range commits {
for _, commit := range limitedCommits {
authorString := commit.Author.String()
if uniqueAuthors.Add(authorString) && authorString != posterSig {
// Compare use account as well to avoid adding the same author multiple times
@@ -886,14 +870,14 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
}
// collect the remaining authors
if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
skip := limit
limit = 30
if limitFirst >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
skip := limitFirst
batchLimit := 30
for {
commits, err = gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, batchLimit, skip)
if err != nil {
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
return ""
return authors
}
if len(commits) == 0 {
break
@@ -907,22 +891,46 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
}
}
}
skip += limit
skip += batchLimit
}
}
return authors
}
func formatSquashMergeCommitMessages(commits []*git.Commit) string {
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
sb := &bytes.Buffer{}
// commits list is in reverse chronological order
for _, commit := range slices.Backward(commits) {
msg := strings.TrimSpace(commit.MessageUTF8())
if msg == "" {
continue
}
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_, _ = fmt.Fprintf(sb, "* %s\n\n", msg)
if maxMsgSize > 0 && sb.Len() >= maxMsgSize {
break
}
}
if stringBuilder.Len() > 0 && len(authors) > 0 {
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above
stringBuilder.WriteString("---------\n\n")
buf := bytes.TrimSpace(sb.Bytes())
if maxMsgSize > 0 && len(buf) > maxMsgSize {
buf = buf[:maxMsgSize]
for {
r, sz := utf8.DecodeLastRune(buf)
if r == utf8.RuneError && sz == 1 {
buf = buf[:len(buf)-1]
continue
}
break
}
buf = append(buf, '.', '.', '.')
}
for _, author := range authors {
stringBuilder.WriteString("Co-authored-by: ")
stringBuilder.WriteString(author)
stringBuilder.WriteRune('\n')
}
return stringBuilder.String()
buf = append(buf, '\n', '\n')
return util.UnsafeBytesToString(buf)
}
// GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
@@ -1074,7 +1082,7 @@ func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user
}
commits = append(commits, CommitInfo{
Summary: commit.Summary(),
Summary: commit.MessageTitle(),
CommitterOrAuthorName: committerOrAuthorName,
ID: commit.ID.String(),
ShortSha: base.ShortSha(commit.ID.String()),
+49 -20
View File
@@ -7,32 +7,37 @@ package pull
import (
"testing"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/gitrepo"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
// TODO TestPullRequest_PushToBaseRepo
func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) {
// Not a valid trailer section
assert.False(t, commitMessageTrailersPattern.MatchString(""))
assert.False(t, commitMessageTrailersPattern.MatchString("No trailer."))
assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>\nNot a trailer due to following text."))
assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob <bob@example.com>"))
// Valid trailer section
assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>"))
assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob <bob@example.com>\nOther-Trailer: Value"))
assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob <bob@example.com>"))
assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob <bob@example.com>\nOther-Trailer: Value"))
assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob <bob@example.com>\n"))
assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob <bob@example.com>"))
assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob <bob@example.com> "))
assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value"))
func TestPullRequest_FormatSquashMergeCommitMessages(t *testing.T) {
oldest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 1"}}
newest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 2\n\nCommit description."}}
defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 0)()
assert.Equal(t, "* commit msg 1\n\n* commit msg 2\n\nCommit description.\n\n", formatSquashMergeCommitMessages([]*git.Commit{newest, oldest}))
utf8Msg := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "🌞"}}
setting.Repository.PullRequest.DefaultMergeMessageSize = 3
assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
setting.Repository.PullRequest.DefaultMergeMessageSize = 4
assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
setting.Repository.PullRequest.DefaultMergeMessageSize = 5
assert.Equal(t, "* ...\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
setting.Repository.PullRequest.DefaultMergeMessageSize = 6
assert.Equal(t, "* 🌞\n\n", formatSquashMergeCommitMessages([]*git.Commit{utf8Msg}))
}
func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
@@ -88,3 +93,27 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage)
}
func TestBuildSquashMergeCommitMessages(t *testing.T) {
cases := []struct {
msg string
coAuthors []string
expected string
}{
{"title", nil, "title"},
{"title", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
{"title\n\n", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
{"title\n\nKey: val", []string{"the-user"}, "title\n\nKey: val\nCo-authored-by: the-user\n"},
{"title\n\n----\nKey: val", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"},
{"title\n\n----\nKey: val\n\n", []string{"the-user"}, "title\n\n----\nKey: val\nCo-authored-by: the-user\n"},
{"title\n\nbody", nil, "title\n\nbody"},
{"title\n\nbody", []string{"the-user"}, "title\n\nbody\n\n---------\n\nCo-authored-by: the-user\n"},
{"title\n\nbody\n\nKey: val", []string{"the-user"}, "title\n\nbody\n\nKey: val\nCo-authored-by: the-user\n"},
{"title\n\nbody\n\n----\nKey: val", []string{"the-user"}, "title\n\nbody\n\n----\nKey: val\nCo-authored-by: the-user\n"},
}
for _, c := range cases {
msg := buildSquashMergeCommitMessages(c.msg, c.coAuthors)
assert.Equal(t, c.expected, msg, "msg: %s", c.msg)
}
}
+15 -15
View File
@@ -10,19 +10,19 @@ import (
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
notify_service "code.gitea.io/gitea/services/notify"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/services/gitdiff"
notify_service "gitea.dev/services/notify"
)
func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
@@ -35,7 +35,7 @@ func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
return notFound || notEnoughLines
}
// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
// ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR.
type ErrDismissRequestOnClosedPR struct{}
// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
@@ -52,7 +52,7 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
// ErrSubmitReviewOnClosedPR represents an error when a user tries to submit an approve or reject review associated to a closed or merged PR.
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
// LineBlame returns the latest commit at the given line
@@ -6,10 +6,10 @@ package pull_test
import (
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
pull_service "code.gitea.io/gitea/services/pull"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
pull_service "gitea.dev/services/pull"
"github.com/stretchr/testify/assert"
)
@@ -6,13 +6,13 @@ package pull
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"xorm.io/builder"
)
@@ -6,9 +6,9 @@ package pull_test
import (
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
pull_service "code.gitea.io/gitea/services/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
pull_service "gitea.dev/services/pull"
"github.com/stretchr/testify/assert"
)
@@ -11,13 +11,13 @@ import (
"os"
"path/filepath"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
repo_module "gitea.dev/modules/repository"
)
// Temporary repos created here use standard branch names to help simplify
+55 -48
View File
@@ -8,20 +8,22 @@ import (
"errors"
"fmt"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/repository"
)
// Update updates pull request with base branch.
func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
func Update(pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
ctx := graceful.GetManager().HammerContext() // don't abort the git operation even if the user's request is canceled
if pr.Flow == issues_model.PullRequestFlowAGit {
// TODO: update of agit flow pull request's head branch is unsupported
return errors.New("update of agit flow pull request's head branch is unsupported")
@@ -62,20 +64,10 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
}
defer func() {
// The code is from https://github.com/go-gitea/gitea/pull/9784,
// it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason.
// TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details
go AddTestPullRequestTask(TestPullRequestOptions{
RepoID: pr.BaseRepo.ID,
Doer: doer,
Branch: pr.BaseBranch,
IsSync: false,
IsForcePush: false,
OldCommitID: "",
NewCommitID: "",
})
}()
// TODO: The code is from https://github.com/go-gitea/gitea/pull/9784,
// it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason.
// TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details
defer addTestPullRequestTaskAfterWebOperation(pr, doer)
if rebase {
return updateHeadByRebaseOnToBase(ctx, pr, doer)
@@ -97,6 +89,13 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
}
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
// TODO: the "update" (merge target branch to PR head branch) operation has finished, there could still be some edge cases:
// * the database was already out of sync: the target branch was already in head branch:
// * so no post-receive hook is really executed, no PR status update
// * then the PR status is stuck in "behind the target branch" (a new push can be used as a workaround)
// * "merge" operation does finish, but the post-receive hook isn't correctly executed due to other reasons:
// * although the target branch is merged into head branch by this "update" (head branch receives new commits)
// * but database isn't updated, so the PR status is still "behind the target branch"
return err
}
@@ -131,74 +130,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 {
@@ -8,14 +8,14 @@ import (
"fmt"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
)
// updateHeadByRebaseOnToBase handles updating a PR's head branch by rebasing it on the PR current base branch
@@ -4,17 +4,18 @@
package pull
import (
"context"
"testing"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
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"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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)