mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-29 18:41:38 +00:00
refactor(automerge): fix error handling, populate recent automerge tasks on restart (#39001)
* Refactor "automerge" related code, clarify many details (including "unique queue item", start check by pull head or commit) * Fix automerge queue handler's error handling, clarify error messages * Populate recent automerge tasks on restart to restore the previous aborted automerge tasks Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
@@ -123,7 +123,9 @@ func (run *ActionRun) RefLink() string {
|
||||
func (run *ActionRun) PrettyRef() string {
|
||||
refName := git.RefName(run.Ref)
|
||||
if refName.IsPull() {
|
||||
return "#" + strings.TrimSuffix(strings.TrimPrefix(run.Ref, git.PullPrefix), "/head")
|
||||
if pullIndex, ok := refName.PullIndex(); ok {
|
||||
return "#" + strconv.FormatInt(pullIndex, 10)
|
||||
}
|
||||
}
|
||||
return refName.ShortName()
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer)
|
||||
|
||||
// GetGitHeadRefName returns git ref for hidden pull request branch
|
||||
func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefName but not string
|
||||
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
|
||||
return git.RefNameFromPullIndex(pr.Index).String()
|
||||
}
|
||||
|
||||
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||
|
||||
@@ -80,6 +80,12 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
||||
return true, scheduledPRM, err
|
||||
}
|
||||
|
||||
func GetScheduledMergePullIDsSince(ctx context.Context, since timeutil.TimeStamp) ([]int64, error) {
|
||||
var pullIDs []int64
|
||||
err := db.GetEngine(ctx).Table(&AutoMerge{}).Where("created_unix >= ?", since).Cols("pull_id").Find(&pullIDs)
|
||||
return pullIDs, err
|
||||
}
|
||||
|
||||
// DeleteScheduledAutoMerge delete a scheduled pull request
|
||||
func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error {
|
||||
exist, scheduledPRM, err := GetScheduledMergeByPullID(ctx, pullID)
|
||||
|
||||
@@ -6,6 +6,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
@@ -18,6 +19,7 @@ const (
|
||||
RemotePrefix = "refs/remotes/"
|
||||
// PullPrefix is the base directory of the pull information of git.
|
||||
PullPrefix = "refs/pull/"
|
||||
pullSuffix = "/head"
|
||||
)
|
||||
|
||||
// refNamePatternInvalid is regular expression with unallowed characters in git reference name
|
||||
@@ -93,6 +95,10 @@ func RefNameFromCommit(shortName string) RefName {
|
||||
return RefName(shortName)
|
||||
}
|
||||
|
||||
func RefNameFromPullIndex(prIndex int64) RefName {
|
||||
return RefName(PullPrefix + strconv.FormatInt(prIndex, 10) + pullSuffix)
|
||||
}
|
||||
|
||||
func (ref RefName) String() string {
|
||||
return string(ref)
|
||||
}
|
||||
@@ -134,14 +140,21 @@ func (ref RefName) BranchName() string {
|
||||
return ref.nameWithoutPrefix(BranchPrefix)
|
||||
}
|
||||
|
||||
// PullName returns the pull request name part of refs like refs/pull/<pull_name>/head
|
||||
func (ref RefName) PullName() string {
|
||||
func (ref RefName) PullIndex() (int64, bool) {
|
||||
refName := string(ref)
|
||||
lastIdx := strings.LastIndexByte(refName[len(PullPrefix):], '/')
|
||||
if strings.HasPrefix(refName, PullPrefix) && lastIdx > -1 {
|
||||
return refName[len(PullPrefix) : lastIdx+len(PullPrefix)]
|
||||
s, ok := strings.CutPrefix(refName, PullPrefix)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return ""
|
||||
pullStr, last, ok := strings.CutLast(s, "/")
|
||||
if !ok || last != "head" {
|
||||
return 0, false
|
||||
}
|
||||
pullIndex, err := strconv.ParseInt(pullStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return pullIndex, true
|
||||
}
|
||||
|
||||
// ForBranchName returns the branch name part of refs like refs/for/<branch_name>
|
||||
@@ -165,7 +178,7 @@ func (ref RefName) ShortName() string {
|
||||
return ref.RemoteName()
|
||||
}
|
||||
if ref.IsPull() {
|
||||
return ref.PullName()
|
||||
return strings.TrimSuffix(ref.nameWithoutPrefix(PullPrefix), pullSuffix)
|
||||
}
|
||||
if ref.IsFor() {
|
||||
return ref.ForBranchName()
|
||||
|
||||
@@ -19,10 +19,12 @@ func TestRefName(t *testing.T) {
|
||||
assert.Equal(t, "release/foo", RefName("refs/tags/release/foo").TagName())
|
||||
|
||||
// Test pull names
|
||||
assert.Equal(t, "1", RefName("refs/pull/1/head").PullName())
|
||||
pullIndex, ok := RefName("refs/pull/1/head").PullIndex()
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, 1, pullIndex)
|
||||
assert.True(t, RefName("refs/pull/1/head").IsPull())
|
||||
assert.True(t, RefName("refs/pull/1/merge").IsPull())
|
||||
assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName())
|
||||
assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").ShortName())
|
||||
|
||||
// Test for branch names
|
||||
assert.Equal(t, "main", RefName("refs/for/main").ForBranchName())
|
||||
|
||||
@@ -152,7 +152,7 @@ func InitWebInstalled(ctx context.Context) {
|
||||
mirror_service.InitSyncMirrors()
|
||||
mustInit(webhook.Init)
|
||||
mustInit(pull_service.Init)
|
||||
mustInit(automerge.Init)
|
||||
mustInitCtx(ctx, automerge.Init)
|
||||
mustInit(task.Init)
|
||||
mustInit(repo_migrations.Init)
|
||||
mustInit(websocket_service.Init)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/services/automergequeue"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
@@ -29,29 +31,40 @@ import (
|
||||
)
|
||||
|
||||
// Init runs the task queue to that handles auto merges
|
||||
func Init() error {
|
||||
func Init(ctx context.Context) error {
|
||||
notify_service.RegisterNotifier(NewNotifier())
|
||||
|
||||
automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
|
||||
automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge",
|
||||
func(items ...automergequeue.AutoMergeItem) (unhandled []automergequeue.AutoMergeItem) {
|
||||
for _, item := range items {
|
||||
handleAutoMergeItem(item)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if automergequeue.AutoMergeQueue == nil {
|
||||
return errors.New("unable to create pr_auto_merge queue")
|
||||
}
|
||||
go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
|
||||
populateRecentAutoMergeItems(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle passed PR IDs and test the PRs
|
||||
func handler(items ...string) []string {
|
||||
for _, s := range items {
|
||||
var id int64
|
||||
var sha string
|
||||
if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil {
|
||||
log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
|
||||
func populateRecentAutoMergeItems(ctx context.Context) {
|
||||
// in case Gitea's restart aborted some scheduled auto-merge pull requests, try to re-start the recent ones
|
||||
pullIDs, err := pull_model.GetScheduledMergePullIDsSince(ctx, timeutil.TimeStampNow().AddDuration(-24*time.Hour))
|
||||
if err != nil {
|
||||
log.Error("Failed to get recent scheduled auto-merge pull requests: %v", err)
|
||||
return
|
||||
}
|
||||
for _, pullID := range pullIDs {
|
||||
pull, err := issues_model.GetPullRequestByID(ctx, pullID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get scheduled pull request [%d]: %v", pullID, err)
|
||||
continue
|
||||
}
|
||||
handlePullRequestAutoMerge(id, sha)
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, pull)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
|
||||
@@ -69,7 +82,7 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_
|
||||
scheduled = err == nil
|
||||
if scheduled {
|
||||
log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, pull)
|
||||
}
|
||||
return scheduled, err
|
||||
}
|
||||
@@ -86,124 +99,85 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *
|
||||
})
|
||||
}
|
||||
|
||||
// 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.IsStatusMergeable()
|
||||
})
|
||||
var errSkipAutoMerge = errors.New("skip auto merge")
|
||||
|
||||
func handleAutoMergeItem(item automergequeue.AutoMergeItem) {
|
||||
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), "AutoMerge: "+string(item))
|
||||
defer finished()
|
||||
|
||||
fields := strings.Split(string(item), ":")
|
||||
if len(fields) != 3 || fields[0] != "pr" {
|
||||
return
|
||||
}
|
||||
pullIDStr, headCommitID := fields[1], fields[2]
|
||||
pullID, _ := strconv.ParseInt(pullIDStr, 10, 64)
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, pullID)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Error("AutoMerge: GetPullRequestByID[%d]: %v", pullID, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pr := range pulls {
|
||||
automergequeue.AddToQueue(pr, sha)
|
||||
err = handlePullRequestAutoMerge(ctx, pr, headCommitID)
|
||||
if errors.Is(err, errSkipAutoMerge) {
|
||||
log.Debug("AutoMerge: skipping pull request [%d] auto merge: %v", pullID, err)
|
||||
} else if err != nil {
|
||||
log.Error("AutoMerge: failed to auto merge pull request [%d]: %v", pullID, err)
|
||||
} else {
|
||||
log.Info("AutoMerge: auto merge pull request [%d]", pullID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 := git.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
refs, err := gitRepo.GetRefsBySha(ctx, sha, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pulls := make(map[int64]*issues_model.PullRequest)
|
||||
|
||||
for _, ref := range refs {
|
||||
// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
|
||||
// use that to get the pr.
|
||||
if strings.HasPrefix(ref, git.PullPrefix) {
|
||||
parts := strings.Split(ref[len(git.PullPrefix):], "/")
|
||||
|
||||
// e.g. 'refs/pull/1/head' would be []string{"1", "head"}
|
||||
if len(parts) != 2 {
|
||||
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
|
||||
continue
|
||||
}
|
||||
|
||||
prIndex, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
|
||||
if err != nil {
|
||||
// If there is no pull request for this branch, we don't try to merge it.
|
||||
if issues_model.IsErrPullRequestNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filter(p) {
|
||||
pulls[p.ID] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pulls, nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
func handlePullRequestAutoMerge(ctx context.Context, pr *issues_model.PullRequest, expectedHeadCommitID string) error {
|
||||
_ = pr.LoadIssue(ctx)
|
||||
if (pr.Issue != nil && pr.Issue.IsClosed) || pr.HasMerged {
|
||||
// if the PR has been closed or merged, delete the automerge record and skip
|
||||
err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
|
||||
if err != nil {
|
||||
return errors.Join(errSkipAutoMerge, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, pullID)
|
||||
if err != nil {
|
||||
log.Error("GetPullRequestByID[%d]: %v", pullID, err)
|
||||
return
|
||||
if !pr.IsStatusMergeable() || pr.IsWorkInProgress(ctx) {
|
||||
// quick check: if the PR can't be merged, just skip
|
||||
return errors.Join(errSkipAutoMerge, errors.New("pull request is not mergeable or is work in progress"))
|
||||
}
|
||||
|
||||
// Check if there is a scheduled pr in the db
|
||||
exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID)
|
||||
if err != nil {
|
||||
log.Error("%-v GetScheduledMergeByPullID: %v", pr, err)
|
||||
return
|
||||
return fmt.Errorf("failed to get scheduled auto-merge: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("pull request doesn't exist"))
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("%-v LoadBaseRepo: %v", pr, err)
|
||||
return
|
||||
return fmt.Errorf("failed to load base repo: %w", err)
|
||||
}
|
||||
if err = pr.LoadHeadRepo(ctx); err != nil {
|
||||
return fmt.Errorf("failed to load head repo: %w", err)
|
||||
}
|
||||
|
||||
// check the sha is the same as pull request head commit id
|
||||
baseGitRepo, err := git.OpenRepository(ctx, pr.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
return fmt.Errorf("failed to open base git repo: %w", err)
|
||||
}
|
||||
defer baseGitRepo.Close()
|
||||
|
||||
headCommitID, err := baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
return fmt.Errorf("failed to get ref commit ID: %w", err)
|
||||
}
|
||||
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
|
||||
if headCommitID != expectedHeadCommitID {
|
||||
return errors.Join(errSkipAutoMerge, errors.New("head commit ID changed"))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
switch pr.Flow {
|
||||
case issues_model.PullRequestFlowGithub:
|
||||
@@ -212,67 +186,58 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
||||
headBranchExist, _ = git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch)
|
||||
}
|
||||
if !headBranchExist {
|
||||
log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("head branch does not exist"))
|
||||
}
|
||||
case issues_model.PullRequestFlowAGit:
|
||||
headBranchExist := git.IsReferenceExist(ctx, pr.BaseRepo, pr.GetGitHeadRefName())
|
||||
if !headBranchExist {
|
||||
log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]", pr, pr.HeadRepoID, pr.HeadBranch)
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("head branch (agit) does not exist"))
|
||||
}
|
||||
default:
|
||||
log.Error("wrong flow type %d", pr.Flow)
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("unsupported pull request git flow type"))
|
||||
}
|
||||
|
||||
// Check if all checks succeeded
|
||||
pass, err := pull_service.IsPullCommitStatusPass(ctx, pr)
|
||||
if err != nil {
|
||||
log.Error("%-v IsPullCommitStatusPass: %v", pr, err)
|
||||
return
|
||||
return fmt.Errorf("failed to check pull commit status: %w", err)
|
||||
}
|
||||
if !pass {
|
||||
log.Info("Scheduled auto merge %-v has unsuccessful status checks", pr)
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("unsuccessful status checks"))
|
||||
}
|
||||
|
||||
// Merge if all checks succeeded
|
||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
||||
_, doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err)
|
||||
return
|
||||
return fmt.Errorf("failed to get scheduled user[%d]: %w", scheduledPRM.DoerID, err)
|
||||
}
|
||||
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("GetDoerRepoPermission %-v: %v", pr.BaseRepo, err)
|
||||
return
|
||||
return fmt.Errorf("failed to get doer repo permission: %w", err)
|
||||
}
|
||||
|
||||
if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, scheduledPRM.MergeStyle, false); err != nil {
|
||||
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
||||
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
||||
return
|
||||
}
|
||||
log.Error("%-v CheckPullMergeable: %v", pr, err)
|
||||
return
|
||||
return errors.Join(errSkipAutoMerge, errors.New("pull request is not mergeable"))
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(pr, doer, 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.
|
||||
// FIXME: if merge failed, we should display some error message to the pull request page, or retry later.
|
||||
// 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
|
||||
return fmt.Errorf("failed to merge PR:%d: %w", pr.ID, err)
|
||||
}
|
||||
|
||||
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, &scheduledPRM.DeleteBranchAfterMerge, pr.BaseRepo, pr)
|
||||
if err != nil {
|
||||
log.Error("ShouldDeleteBranchAfterMerge: %v", err)
|
||||
} else if deleteBranchAfterMerge {
|
||||
if err = repo_service.DeleteBranchAfterMerge(ctx, doer, pr.ID, nil); err != nil {
|
||||
log.Error("DeleteBranchAfterMerge: %v", err)
|
||||
// the PR has been merged, so no error should be returned after this point
|
||||
{
|
||||
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, &scheduledPRM.DeleteBranchAfterMerge, pr.BaseRepo, pr)
|
||||
if err != nil {
|
||||
log.Error("ShouldDeleteBranchAfterMerge: %v", err)
|
||||
} else if deleteBranchAfterMerge {
|
||||
if err = repo_service.DeleteBranchAfterMerge(ctx, doer, pr.ID, nil); err != nil {
|
||||
log.Error("DeleteBranchAfterMerge: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.dev/modules/repository"
|
||||
"gitea.dev/services/automergequeue"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
)
|
||||
|
||||
type automergeNotifier struct {
|
||||
@@ -30,9 +31,7 @@ func NewNotifier() notify_service.Notifier {
|
||||
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)
|
||||
}
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, pr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +45,20 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
|
||||
return
|
||||
}
|
||||
// as reviews could have blocked a pending automerge let's recheck
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, review.Issue.PullRequest)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if !status.State.IsSuccess() {
|
||||
return
|
||||
}
|
||||
|
||||
pulls, err := pull_service.GetMergeablePullRequestsByHeadCommitID(ctx, repo, commit.Sha1)
|
||||
if err != nil {
|
||||
log.Error("GetMergeablePullRequestsByHeadCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
for _, pr := range pulls {
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, pr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package automergequeue
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/git"
|
||||
@@ -14,21 +14,23 @@ import (
|
||||
"gitea.dev/modules/queue"
|
||||
)
|
||||
|
||||
var AutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
// AutoMergeItem is for the unique queue, so the item type can't be JSON which doesn't have deterministic key order.
|
||||
// Since the queue is a unique queue, the item must contain commit ID, otherwise the new commit ID will be ignored.
|
||||
type AutoMergeItem string
|
||||
|
||||
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil && !errors.Is(err, queue.ErrAlreadyInQueue) {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
var AutoMergeQueue *queue.WorkerPoolQueue[AutoMergeItem]
|
||||
|
||||
var AddToQueue = func(item AutoMergeItem) {
|
||||
if err := AutoMergeQueue.Push(item); err != nil && !errors.Is(err, queue.ErrAlreadyInQueue) {
|
||||
log.Error("Error adding %v to the automerge queue: %v", item, err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.IsStatusMergeable() {
|
||||
return
|
||||
}
|
||||
func StartAutoMergeCheckByPullCommit(pullID int64, commitID string) {
|
||||
AddToQueue(AutoMergeItem("pr:" + strconv.FormatInt(pullID, 10) + ":" + commitID))
|
||||
}
|
||||
|
||||
func StartAutoMergeCheckByPullHead(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
@@ -40,11 +42,11 @@ func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullReques
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commitID, err := gitRepo.GetRefCommitID(ctx, pull.GetGitHeadRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
AddToQueue(pull, commitID)
|
||||
StartAutoMergeCheckByPullCommit(pull.ID, commitID)
|
||||
}
|
||||
|
||||
@@ -348,8 +348,8 @@ func loadPullRequestsForRun(ctx context.Context, run *actions_model.ActionRun) (
|
||||
var prs issues_model.PullRequestList
|
||||
switch {
|
||||
case run.Event.IsPullRequest() || run.Event.IsPullRequestReview():
|
||||
index, err := strconv.ParseInt(refName.PullName(), 10, 64)
|
||||
if err != nil {
|
||||
index, ok := refName.PullIndex()
|
||||
if !ok {
|
||||
return result, nil
|
||||
}
|
||||
pr, err := issues_model.GetPullRequestByIndex(ctx, run.RepoID, index)
|
||||
|
||||
@@ -297,7 +297,7 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
|
||||
} else if !exist {
|
||||
return
|
||||
}
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
|
||||
automergequeue.StartAutoMergeCheckByPullHead(ctx, pr)
|
||||
}
|
||||
|
||||
// getMergeCommit checks if a pull request has been merged
|
||||
|
||||
@@ -117,10 +117,9 @@ func TestMarkPullRequestAsMergeable(t *testing.T) {
|
||||
prPatchCheckerQueue = nil
|
||||
}()
|
||||
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
})()
|
||||
addToQueuePullChan := make(chan automergequeue.AutoMergeItem, 1)
|
||||
defer test.MockVariableValue(&automergequeue.AddToQueue, func(item automergequeue.AutoMergeItem) { addToQueuePullChan <- item })()
|
||||
|
||||
ctx := t.Context()
|
||||
_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
@@ -140,8 +139,8 @@ func TestMarkPullRequestAsMergeable(t *testing.T) {
|
||||
require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
|
||||
|
||||
select {
|
||||
case sha := <-addToQueueShaChan:
|
||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
|
||||
case item := <-addToQueuePullChan:
|
||||
assert.EqualValues(t, "pr:2:985f0301dba5e7b34be866819cd15ad3d8f508ee", item) // ref: refs/pull/3/head
|
||||
case <-time.After(1 * time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
|
||||
55
services/pull/ref.go
Normal file
55
services/pull/ref.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
func EnumPullRequestsByHeadCommitID(ctx context.Context, repo *repo_model.Repository, commitID string, filter func(*issues_model.PullRequest) bool) (pulls []*issues_model.PullRequest, _ error) {
|
||||
gitRepo, err := git.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
refs, err := gitRepo.GetRefsBySha(ctx, commitID, git.PullPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, refStr := range refs {
|
||||
ref := git.RefName(refStr)
|
||||
prIndex, ok := ref.PullIndex()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
pull, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
continue // ignore non-existing pull requests
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filter(pull) {
|
||||
pulls = append(pulls, pull)
|
||||
}
|
||||
}
|
||||
return pulls, nil
|
||||
}
|
||||
|
||||
func GetMergeablePullRequestsByHeadCommitID(ctx context.Context, repo *repo_model.Repository, commitID string) ([]*issues_model.PullRequest, error) {
|
||||
return EnumPullRequestsByHeadCommitID(ctx, repo, commitID, func(pr *issues_model.PullRequest) bool {
|
||||
_ = pr.LoadIssue(ctx)
|
||||
return pr.Issue != nil && !pr.Issue.IsClosed && !pr.HasMerged && pr.IsStatusMergeable()
|
||||
})
|
||||
}
|
||||
22
services/pull/ref_test.go
Normal file
22
services/pull/ref_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/services/pull"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetMergeablePullRequestsByHeadCommitID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
pulls, err := pull.GetMergeablePullRequestsByHeadCommitID(t.Context(), repo1, "985f0301dba5e7b34be866819cd15ad3d8f508ee")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pulls, 1)
|
||||
}
|
||||
@@ -808,22 +808,20 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
oldAutoMergeAddToQueue := automergequeue.AddToQueue
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
automergequeue.AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
}
|
||||
addToQueuePullChan := make(chan automergequeue.AutoMergeItem, 1)
|
||||
resetAutoMergeQueueMock := test.MockVariableValue(&automergequeue.AddToQueue, func(item automergequeue.AutoMergeItem) { addToQueuePullChan <- item })
|
||||
|
||||
// first time insert automerge record, return true
|
||||
scheduled, err := automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, scheduled)
|
||||
// and the pr should be added to automergequeue, in case it is already "mergeable"
|
||||
select {
|
||||
case <-addToQueueShaChan:
|
||||
case <-addToQueuePullChan:
|
||||
case <-time.After(time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
automergequeue.AddToQueue = oldAutoMergeAddToQueue
|
||||
resetAutoMergeQueueMock()
|
||||
|
||||
// second time insert automerge record, return false because it does exist
|
||||
scheduled, err = automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
|
||||
Reference in New Issue
Block a user