diff --git a/cmd/hook.go b/cmd/hook.go index b741127ca3c17..4c6fb3fd3a163 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -196,6 +196,7 @@ Gitea or set your environment appropriately.`, "") PullRequestID: prID, DeployKeyID: deployKeyID, ActionPerm: int(actionPerm), + PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), } scanner := bufio.NewScanner(os.Stdin) diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index e8bef7d6c14bb..918363018d2ff 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -321,7 +321,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { } func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) { - return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID) + return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetPossibleUserByID) } // handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index a60883b4cc501..4710e5c76d0ab 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" 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" @@ -247,17 +248,16 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { } // Merge if all checks succeeded - doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) + // Use GetPossibleUserByID to allow merging by deleted users or bot users + doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) if err != nil { log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err) return } - perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) - if err != nil { - log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err) - return - } + // We don't check doer's permission here because their permissions have been checked + // before the pull request was scheduled to auto merge + perm := access_model.Permission{AccessMode: perm.AccessModeWrite} if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { if errors.Is(err, pull_service.ErrNotReadyToMerge) { diff --git a/services/automerge/notify.go b/services/automerge/notify.go index 8a1bb5fc90c67..3c027e211be3b 100644 --- a/services/automerge/notify.go +++ b/services/automerge/notify.go @@ -31,7 +31,7 @@ func (n *automergeNotifier) PullRequestReview(ctx context.Context, pr *issues_mo // as a missing / blocking reviews could have blocked a pending automerge let's recheck if review.Type == issues_model.ReviewTypeApprove { if err := StartPRCheckAndAutoMergeBySHA(ctx, review.CommitID, pr.BaseRepo); err != nil { - log.Error("StartPullRequestAutoMergeCheckBySHA: %v", err) + log.Error("StartPRCheckAndAutoMergeBySHA: %v", err) } } } @@ -52,7 +52,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { if status.State.IsSuccess() { if err := StartPRCheckAndAutoMergeBySHA(ctx, commit.Sha1, repo); err != nil { - log.Error("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, sender.ID, commit.Sha1, err) + log.Error("StartPRCheckAndAutoMergeBySHA[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, sender.ID, commit.Sha1, err) } } } diff --git a/services/pull/merge.go b/services/pull/merge.go index a941c204357ac..02081d2554490 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -19,6 +19,7 @@ import ( "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" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" @@ -400,6 +401,9 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use ) mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) + if pushTrigger == repo_module.PushTriggerPRMergeToBase { + mergeCtx.env = append(mergeCtx.env, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite)) + } pushCmd := git.NewCommand("push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) // Push back to upstream. diff --git a/tests/integration/api_pull_merge_test.go b/tests/integration/api_pull_merge_test.go new file mode 100644 index 0000000000000..e1672dfc2f7a4 --- /dev/null +++ b/tests/integration/api_pull_merge_test.go @@ -0,0 +1,107 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + pull_model "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/commitstatus" + "code.gitea.io/gitea/modules/gitrepo" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" + + "github.com/stretchr/testify/assert" +) + +func TestAPIPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // create a pull request + session := loginUser(t, "user1") + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + forkedName := "repo1-1" + testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "") + defer func() { + testDeleteRepository(t, session, "user1", forkedName) + }() + testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n") + testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull") + + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName}) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ + BaseRepoID: baseRepo.ID, + BaseBranch: "master", + HeadRepoID: forkedRepo.ID, + HeadBranch: "master", + }) + + // add protected branch for commit status + csrf := GetUserCSRFToken(t, session) + // Change master branch to protected + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ + "_csrf": csrf, + "rule_name": "master", + "enable_push": "true", + "enable_status_check": "true", + "status_check_contexts": "gitea/actions", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // add automerge for this repo + req = NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", baseRepo.OwnerName, baseRepo.Name, pr.Index), + strings.NewReader(url.Values{ + "do": []string{"merge"}, + "merge_when_checks_succeed": []string{"true"}, + }.Encode())). + AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + MakeRequest(t, req, http.StatusCreated) + + // reload pr again + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) + assert.False(t, pr.HasMerged) + assert.Empty(t, pr.MergedCommitID) + + // update commit status to success, then it should be merged automatically + baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo) + assert.NoError(t, err) + sha, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + assert.NoError(t, err) + masterCommitID, err := baseGitRepo.GetBranchCommitID("master") + assert.NoError(t, err) + + branches, _, err := baseGitRepo.GetBranchNames(0, 100) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches) + baseGitRepo.Close() + defer func() { + testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID) + }() + + err = commitstatus_service.CreateCommitStatus(t.Context(), baseRepo, user1, sha, &git_model.CommitStatus{ + State: commitstatus.CommitStatusSuccess, + TargetURL: "https://gitea.com", + Context: "gitea/actions", + }) + assert.NoError(t, err) + + time.Sleep(2 * time.Second) + + // reload pr again + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) + assert.True(t, pr.HasMerged) + assert.NotEmpty(t, pr.MergedCommitID) + + unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID}) + }) +}