Skip to content

Fix automerge will not work because of some events haven't been triggered #30780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f51af18
wip yES i Know theres is a cycle
6543 Jul 7, 2023
d145c26
fix & finish
6543 Mar 2, 2024
0dc9531
Use notify to trigger auto merge check
lunny Apr 18, 2024
e4e24d2
Remove unnecessary changes
lunny Apr 18, 2024
db6e278
Fix bugs
lunny Apr 23, 2024
a5d0708
Fix bug
lunny Apr 23, 2024
b208bde
Fix lint
lunny Apr 23, 2024
c1eac83
Add transaction for auto merge
lunny Apr 24, 2024
3ec2753
Close git repo earlier
lunny Apr 30, 2024
b541467
Merge branch 'main' into lunny/fix_automerge
lunny May 7, 2024
ed361d2
Revert unnecessary change
lunny May 7, 2024
f5e4176
Revert unnecessary change
lunny May 7, 2024
531e8db
Revert unnecessary change
lunny May 7, 2024
9331ccd
Use better names for the automerge functions
lunny May 7, 2024
8ecfb7d
Merge branch 'main' into lunny/fix_automerge
lunny May 7, 2024
7d14dfc
Improve the comments
lunny May 7, 2024
51e4888
Merge branch 'main' into lunny/fix_automerge
lunny May 9, 2024
797236c
Add tests for pull request automerge when updating or commit status s…
lunny May 10, 2024
5c0f00a
Fix test
lunny May 10, 2024
44b306a
Fix test
lunny May 11, 2024
ee1aba3
Merge branch 'main' into lunny/fix_automerge
lunny May 11, 2024
0d4cce7
Fix tests
lunny May 11, 2024
438d880
Fix test
lunny May 11, 2024
fd8dda3
improvment
lunny May 13, 2024
93c9739
Merge branch 'main' into lunny/fix_automerge
lunny May 13, 2024
deeea79
add trace for test
lunny May 18, 2024
8331fa0
Add trace
lunny May 18, 2024
d9e8a42
add trace for test
lunny May 19, 2024
e447ec2
improve test
lunny May 19, 2024
8d98ed5
Merge branch 'main' into lunny/fix_automerge
lunny May 20, 2024
a0adfd5
Fix test
lunny May 20, 2024
1c202fa
Fix test
lunny May 21, 2024
56914ed
Fix test
lunny May 21, 2024
63b74a2
Merge branch 'main' into lunny/fix_automerge
lunny May 21, 2024
a5f0397
Use pull request directly when pull request review approved
lunny May 21, 2024
9680940
Revert "Use pull request directly when pull request review approved"
lunny May 21, 2024
894537c
Update services/automerge/automerge.go
lunny May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions models/issues/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,14 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) {
if r.CodeComments != nil {
return err
}
if err = r.loadIssue(ctx); err != nil {
if err = r.LoadIssue(ctx); err != nil {
return err
}
r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false)
return err
}

func (r *Review) loadIssue(ctx context.Context) (err error) {
func (r *Review) LoadIssue(ctx context.Context) (err error) {
if r.Issue != nil {
return err
}
Expand Down Expand Up @@ -199,7 +199,7 @@ func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {

// LoadAttributes loads all attributes except CodeComments
func (r *Review) LoadAttributes(ctx context.Context) (err error) {
if err = r.loadIssue(ctx); err != nil {
if err = r.LoadIssue(ctx); err != nil {
return err
}
if err = r.LoadCodeComments(ctx); err != nil {
Expand Down
108 changes: 70 additions & 38 deletions services/automerge/automerge.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
)

Expand All @@ -30,6 +31,8 @@ var prAutoMergeQueue *queue.WorkerPoolQueue[string]

// Init runs the task queue to that handles auto merges
func Init() error {
notify_service.RegisterNotifier(NewNotifier())

prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if prAutoMergeQueue == nil {
return fmt.Errorf("unable to create pr_auto_merge queue")
Expand All @@ -47,7 +50,7 @@ func handler(items ...string) []string {
log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
continue
}
handlePull(id, sha)
handlePullRequestAutoMerge(id, sha)
}
return nil
}
Expand All @@ -62,16 +65,6 @@ func addToQueue(pr *issues_model.PullRequest, sha string) {
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error {
lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
if err != nil {
return err
}

// we don't need to schedule
if lastCommitStatus.IsSuccess() {
return nil
}

if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
return err
}
Expand All @@ -95,8 +88,8 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *
})
}

// MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
// StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
return !pr.HasMerged && pr.CanAutoMerge()
})
Expand All @@ -111,6 +104,32 @@ func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model
return nil
}

// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
Copy link
Contributor

@wxiaoguang wxiaoguang May 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make StartPRCheckAndAutoMergeBySHA call StartPRCheckAndAutoMerge to avoid duplicate code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are two different functions. I think less code are duplicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I don't see why sha should be passed into addToQueue

It seems that the sha is only used for logging. Ideally only call addToQueue(pr.ID) should be enough, and a lot of code could be simplified?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the SHA will prevent when both an automerge task and a newly push are triggered. So the previous triggered automerge task will be ignored.

if headCommitID != sha {
		log.Warn("Head commit id of auto merge %-v does not match sha [%s], it may means the head branch has been updated. Just ignore this request because a new request expected in the queue", pr, sha)
		return
	}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually such check doesn't seem right and only makes some problem harder to debug.

Because, the head commit ID could change again after that check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's not perfect. It needs more refactor like I said before. But since it's a bugfix, I will not do it in this PR.

if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}

if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}

gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}

addToQueue(pull, commitID)
}

func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
Expand Down Expand Up @@ -161,7 +180,8 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
return pulls, nil
}

func handlePull(pullID int64, sha string) {
// handlePullRequestAutoMerge merge the pull request if all checks are successful
func handlePullRequestAutoMerge(pullID int64, sha string) {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
defer finished()
Expand All @@ -182,24 +202,50 @@ func handlePull(pullID int64, sha string) {
return
}

if err = pr.LoadBaseRepo(ctx); err != nil {
log.Error("%-v LoadBaseRepo: %v", pr, err)
return
}

// check the sha is the same as pull request head commit id
baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer baseGitRepo.Close()

headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
if headCommitID != sha {
log.Warn("Head commit id of auto merge %-v does not match sha [%s], it may means the head branch has been updated. Just ignore this request because a new request expected in the queue", pr, sha)
return
}

// Get all checks for this pr
// We get the latest sha commit hash again to handle the case where the check of a previous push
// did not succeed or was not finished yet.

if err = pr.LoadHeadRepo(ctx); err != nil {
log.Error("%-v LoadHeadRepo: %v", pr, err)
return
}

headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
return
var headGitRepo *git.Repository
if pr.BaseRepoID == pr.HeadRepoID {
headGitRepo = baseGitRepo
} else {
headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
return
}
defer headGitRepo.Close()
}
defer headGitRepo.Close()

headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)

if pr.HeadRepo == nil || !headBranchExist {
log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
return
Expand Down Expand Up @@ -238,25 +284,11 @@ func handlePull(pullID int64, sha string) {
return
}

var baseGitRepo *git.Repository
if pr.BaseRepoID == pr.HeadRepoID {
baseGitRepo = headGitRepo
} else {
if err = pr.LoadBaseRepo(ctx); err != nil {
log.Error("%-v LoadBaseRepo: %v", pr, err)
return
}

baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
log.Error("OpenRepository %-v: %v", pr.BaseRepo, err)
return
}
defer baseGitRepo.Close()
}

if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
log.Error("pull_service.Merge: %v", err)
// FIXME: if merge failed, we should display some error message to the pull request page.
// The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
// on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
return
}
}
46 changes: 46 additions & 0 deletions services/automerge/notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package automerge

import (
"context"

issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
notify_service "code.gitea.io/gitea/services/notify"
)

type automergeNotifier struct {
notify_service.NullNotifier
}

var _ notify_service.Notifier = &automergeNotifier{}

// NewNotifier create a new automergeNotifier notifier
func NewNotifier() notify_service.Notifier {
return &automergeNotifier{}
}

func (n *automergeNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
// 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)
}
}
}

func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
if err := review.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
}
if err := review.Issue.LoadPullRequest(ctx); err != nil {
log.Error("LoadPullRequest: %v", err)
return
}
// as reviews could have blocked a pending automerge let's recheck
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
}
2 changes: 1 addition & 1 deletion services/repository/commitstatus/commitstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
}

if status.State.IsSuccess() {
if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
}
}
Expand Down
35 changes: 20 additions & 15 deletions tests/integration/editor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package integration

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -19,25 +20,29 @@ import (
func TestCreateFile(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
})
}

// Request editor page
req := NewRequest(t, "GET", "/user2/repo1/_new/master/")
resp := session.MakeRequest(t, req, http.StatusOK)
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder {
// Request editor page
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
req := NewRequest(t, "GET", newURL)
resp := session.MakeRequest(t, req, http.StatusOK)

doc := NewHTMLParser(t, resp.Body)
lastCommit := doc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
doc := NewHTMLParser(t, resp.Body)
lastCommit := doc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)

// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "test.txt",
"content": "Content",
"commit_choice": "direct",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// Save new file to master branch
req = NewRequestWithValues(t, "POST", newURL, map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath,
"content": content,
"commit_choice": "direct",
})
return session.MakeRequest(t, req, http.StatusSeeOther)
}

func TestCreateFileOnProtectedBranch(t *testing.T) {
Expand Down
Loading