2022-05-07 19:05:52 +02:00
// Copyright 2021 Gitea. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2022-05-07 19:05:52 +02:00
package automerge
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
2022-05-08 15:46:34 +02:00
"code.gitea.io/gitea/models/db"
2025-10-25 10:08:25 -07:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 17:37:59 +08:00
issues_model "code.gitea.io/gitea/models/issues"
2022-05-11 18:09:36 +08:00
access_model "code.gitea.io/gitea/models/perm/access"
2022-05-07 19:05:52 +02:00
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
2024-01-28 04:09:51 +08:00
"code.gitea.io/gitea/modules/gitrepo"
2022-05-07 19:05:52 +02:00
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
2025-07-08 22:51:16 +08:00
"code.gitea.io/gitea/services/automergequeue"
2024-05-21 23:23:22 +08:00
notify_service "code.gitea.io/gitea/services/notify"
2022-05-07 19:05:52 +02:00
pull_service "code.gitea.io/gitea/services/pull"
2025-01-09 11:51:03 -08:00
repo_service "code.gitea.io/gitea/services/repository"
2022-05-07 19:05:52 +02:00
)
// Init runs the task queue to that handles auto merges
func Init ( ) error {
2024-05-21 23:23:22 +08:00
notify_service . RegisterNotifier ( NewNotifier ( ) )
2025-07-08 22:51:16 +08:00
automergequeue . AutoMergeQueue = queue . CreateUniqueQueue ( graceful . GetManager ( ) . ShutdownContext ( ) , "pr_auto_merge" , handler )
if automergequeue . AutoMergeQueue == nil {
2025-04-01 12:14:01 +02:00
return errors . New ( "unable to create pr_auto_merge queue" )
2022-05-07 19:05:52 +02:00
}
2025-07-08 22:51:16 +08:00
go graceful . GetManager ( ) . RunWithCancel ( automergequeue . AutoMergeQueue )
2022-05-07 19:05:52 +02:00
return nil
}
// handle passed PR IDs and test the PRs
2023-05-08 19:49:59 +08:00
func handler ( items ... string ) [ ] string {
for _ , s := range items {
2022-05-07 19:05:52 +02:00
var id int64
var sha string
2023-05-08 19:49:59 +08:00
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 )
2022-05-07 19:05:52 +02:00
continue
}
2024-05-21 23:23:22 +08:00
handlePullRequestAutoMerge ( id , sha )
2022-05-07 19:05:52 +02:00
}
return nil
}
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
2025-01-09 11:51:03 -08:00
func ScheduleAutoMerge ( ctx context . Context , doer * user_model . User , pull * issues_model . PullRequest , style repo_model . MergeStyle , message string , deleteBranchAfterMerge bool ) ( scheduled bool , err error ) {
2022-11-13 04:18:50 +08:00
err = db . WithTx ( ctx , func ( ctx context . Context ) error {
2025-01-09 11:51:03 -08:00
if err := pull_model . ScheduleAutoMerge ( ctx , doer , pull . ID , style , message , deleteBranchAfterMerge ) ; err != nil {
2022-05-08 15:46:34 +02:00
return err
}
2022-06-13 17:37:59 +08:00
_ , err = issues_model . CreateAutoMergeComment ( ctx , issues_model . CommentTypePRScheduledToAutoMerge , pull , doer )
2022-05-08 15:46:34 +02:00
return err
2022-11-13 04:18:50 +08:00
} )
2025-07-08 22:51:16 +08:00
// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
// If the transaction rolls back, then the pull request is not scheduled to auto merge.
// So we should only set "scheduled" to true if there is no error.
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 )
}
2022-06-20 12:02:49 +02:00
return scheduled , err
2022-05-08 15:46:34 +02:00
}
// RemoveScheduledAutoMerge cancels a previously scheduled pull request
2022-06-13 17:37:59 +08:00
func RemoveScheduledAutoMerge ( ctx context . Context , doer * user_model . User , pull * issues_model . PullRequest ) error {
2022-11-13 04:18:50 +08:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2022-05-08 15:46:34 +02:00
if err := pull_model . DeleteScheduledAutoMerge ( ctx , pull . ID ) ; err != nil {
return err
}
2022-06-13 17:37:59 +08:00
_ , err := issues_model . CreateAutoMergeComment ( ctx , issues_model . CommentTypePRUnScheduledToAutoMerge , pull , doer )
2022-05-08 15:46:34 +02:00
return err
2022-11-13 04:18:50 +08:00
} )
2022-05-07 19:05:52 +02:00
}
2024-05-21 23:23:22 +08:00
// 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 {
2022-06-13 17:37:59 +08:00
pulls , err := getPullRequestsByHeadSHA ( ctx , sha , repo , func ( pr * issues_model . PullRequest ) bool {
2022-05-07 19:05:52 +02:00
return ! pr . HasMerged && pr . CanAutoMerge ( )
} )
if err != nil {
return err
}
for _ , pr := range pulls {
2025-07-08 22:51:16 +08:00
automergequeue . AddToQueue ( pr , sha )
2022-05-07 19:05:52 +02:00
}
return nil
}
2022-06-13 17:37:59 +08:00
func getPullRequestsByHeadSHA ( ctx context . Context , sha string , repo * repo_model . Repository , filter func ( * issues_model . PullRequest ) bool ) ( map [ int64 ] * issues_model . PullRequest , error ) {
2024-01-28 04:09:51 +08:00
gitRepo , err := gitrepo . OpenRepository ( ctx , repo )
2022-05-07 19:05:52 +02:00
if err != nil {
return nil , err
}
defer gitRepo . Close ( )
refs , err := gitRepo . GetRefsBySha ( sha , "" )
if err != nil {
return nil , err
}
2022-06-13 17:37:59 +08:00
pulls := make ( map [ int64 ] * issues_model . PullRequest )
2022-05-07 19:05:52 +02:00
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
}
2022-06-13 17:37:59 +08:00
p , err := issues_model . GetPullRequestByIndex ( ctx , repo . ID , prIndex )
2022-05-07 19:05:52 +02:00
if err != nil {
// If there is no pull request for this branch, we don't try to merge it.
2022-06-13 17:37:59 +08:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2022-05-07 19:05:52 +02:00
continue
}
return nil , err
}
if filter ( p ) {
pulls [ p . ID ] = p
}
}
}
return pulls , nil
}
2024-05-21 23:23:22 +08:00
// handlePullRequestAutoMerge merge the pull request if all checks are successful
func handlePullRequestAutoMerge ( pullID int64 , sha string ) {
2022-05-07 19:05:52 +02:00
ctx , _ , finished := process . GetManager ( ) . AddContext ( graceful . GetManager ( ) . HammerContext ( ) ,
2023-02-03 23:11:48 +00:00
fmt . Sprintf ( "Handle AutoMerge of PR[%d] with sha[%s]" , pullID , sha ) )
2022-05-07 19:05:52 +02:00
defer finished ( )
2022-06-13 17:37:59 +08:00
pr , err := issues_model . GetPullRequestByID ( ctx , pullID )
2022-05-07 19:05:52 +02:00
if err != nil {
log . Error ( "GetPullRequestByID[%d]: %v" , pullID , err )
return
}
// Check if there is a scheduled pr in the db
exists , scheduledPRM , err := pull_model . GetScheduledMergeByPullID ( ctx , pr . ID )
if err != nil {
2023-02-03 23:11:48 +00:00
log . Error ( "%-v GetScheduledMergeByPullID: %v" , pr , err )
2022-05-07 19:05:52 +02:00
return
}
if ! exists {
return
}
2024-05-21 23:23:22 +08:00
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 ( )
2025-07-16 21:33:33 +08:00
headCommitID , err := baseGitRepo . GetRefCommitID ( pr . GetGitHeadRefName ( ) )
2024-05-21 23:23:22 +08:00
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
}
2022-05-07 19:05:52 +02:00
// 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.
2022-11-19 09:12:33 +01:00
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2023-02-03 23:11:48 +00:00
log . Error ( "%-v LoadHeadRepo: %v" , pr , err )
2022-05-07 19:05:52 +02:00
return
}
2024-08-20 14:17:21 +08:00
switch pr . Flow {
case issues_model . PullRequestFlowGithub :
2025-10-25 10:08:25 -07:00
headBranchExist := pr . HeadRepo != nil
if headBranchExist {
headBranchExist , _ = git_model . IsBranchExist ( ctx , pr . HeadRepo . ID , pr . HeadBranch )
}
2025-03-15 19:48:59 -07:00
if ! headBranchExist {
2024-08-20 14:17:21 +08:00
log . Warn ( "Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]" , pr , pr . HeadRepoID , pr . HeadBranch )
return
}
case issues_model . PullRequestFlowAGit :
2025-07-16 21:33:33 +08:00
headBranchExist := gitrepo . IsReferenceExist ( ctx , pr . BaseRepo , pr . GetGitHeadRefName ( ) )
2024-08-20 14:17:21 +08:00
if ! headBranchExist {
log . Warn ( "Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]" , pr , pr . HeadRepoID , pr . HeadBranch )
return
}
default :
log . Error ( "wrong flow type %d" , pr . Flow )
2022-05-07 19:05:52 +02:00
return
}
// Check if all checks succeeded
pass , err := pull_service . IsPullCommitStatusPass ( ctx , pr )
if err != nil {
2023-02-03 23:11:48 +00:00
log . Error ( "%-v IsPullCommitStatusPass: %v" , pr , err )
2022-05-07 19:05:52 +02:00
return
}
if ! pass {
2023-02-03 23:11:48 +00:00
log . Info ( "Scheduled auto merge %-v has unsuccessful status checks" , pr )
2022-05-07 19:05:52 +02:00
return
}
// Merge if all checks succeeded
2022-12-03 10:48:26 +08:00
doer , err := user_model . GetUserByID ( ctx , scheduledPRM . DoerID )
2022-05-07 19:05:52 +02:00
if err != nil {
2023-02-03 23:11:48 +00:00
log . Error ( "Unable to get scheduled User[%d]: %v" , scheduledPRM . DoerID , err )
2022-05-07 19:05:52 +02:00
return
}
2026-03-29 11:21:14 +02:00
perm , err := access_model . GetDoerRepoPermission ( ctx , pr . BaseRepo , doer )
2022-05-07 19:05:52 +02:00
if err != nil {
2026-03-29 11:21:14 +02:00
log . Error ( "GetDoerRepoPermission %-v: %v" , pr . BaseRepo , err )
2022-05-07 19:05:52 +02:00
return
}
2024-05-09 01:11:43 +09:00
if err := pull_service . CheckPullMergeable ( ctx , doer , & perm , pr , pull_service . MergeCheckTypeGeneral , false ) ; err != nil {
2025-04-24 21:26:57 +02:00
if errors . Is ( err , pull_service . ErrNotReadyToMerge ) {
2023-02-03 23:11:48 +00:00
log . Info ( "%-v was scheduled to automerge by an unauthorized user" , pr )
2022-05-07 19:05:52 +02:00
return
}
2024-05-09 01:11:43 +09:00
log . Error ( "%-v CheckPullMergeable: %v" , pr , err )
2022-05-07 19:05:52 +02:00
return
}
2025-10-31 21:56:08 -07:00
if err := pull_service . Merge ( ctx , pr , doer , scheduledPRM . MergeStyle , "" , scheduledPRM . Message , true ) ; err != nil {
2022-05-07 19:05:52 +02:00
log . Error ( "pull_service.Merge: %v" , err )
2024-05-21 23:23:22 +08:00
// 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.
2022-05-07 19:05:52 +02:00
return
}
2025-01-09 11:51:03 -08:00
2025-10-21 22:06:56 -07:00
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 )
2025-01-09 11:51:03 -08:00
}
}
2022-05-07 19:05:52 +02:00
}