code.gitea.io/gitea@v1.22.3/services/automerge/automerge.go (about)

     1  // Copyright 2021 Gitea. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package automerge
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models/db"
    14  	issues_model "code.gitea.io/gitea/models/issues"
    15  	access_model "code.gitea.io/gitea/models/perm/access"
    16  	pull_model "code.gitea.io/gitea/models/pull"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/gitrepo"
    21  	"code.gitea.io/gitea/modules/graceful"
    22  	"code.gitea.io/gitea/modules/log"
    23  	"code.gitea.io/gitea/modules/process"
    24  	"code.gitea.io/gitea/modules/queue"
    25  	notify_service "code.gitea.io/gitea/services/notify"
    26  	pull_service "code.gitea.io/gitea/services/pull"
    27  )
    28  
    29  // prAutoMergeQueue represents a queue to handle update pull request tests
    30  var prAutoMergeQueue *queue.WorkerPoolQueue[string]
    31  
    32  // Init runs the task queue to that handles auto merges
    33  func Init() error {
    34  	notify_service.RegisterNotifier(NewNotifier())
    35  
    36  	prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
    37  	if prAutoMergeQueue == nil {
    38  		return fmt.Errorf("unable to create pr_auto_merge queue")
    39  	}
    40  	go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
    41  	return nil
    42  }
    43  
    44  // handle passed PR IDs and test the PRs
    45  func handler(items ...string) []string {
    46  	for _, s := range items {
    47  		var id int64
    48  		var sha string
    49  		if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil {
    50  			log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
    51  			continue
    52  		}
    53  		handlePullRequestAutoMerge(id, sha)
    54  	}
    55  	return nil
    56  }
    57  
    58  func addToQueue(pr *issues_model.PullRequest, sha string) {
    59  	log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
    60  	if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
    61  		log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
    62  	}
    63  }
    64  
    65  // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
    66  func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
    67  	err = db.WithTx(ctx, func(ctx context.Context) error {
    68  		if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
    69  			return err
    70  		}
    71  		scheduled = true
    72  
    73  		_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
    74  		return err
    75  	})
    76  	return scheduled, err
    77  }
    78  
    79  // RemoveScheduledAutoMerge cancels a previously scheduled pull request
    80  func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
    81  	return db.WithTx(ctx, func(ctx context.Context) error {
    82  		if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil {
    83  			return err
    84  		}
    85  
    86  		_, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer)
    87  		return err
    88  	})
    89  }
    90  
    91  // StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
    92  func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
    93  	pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
    94  		return !pr.HasMerged && pr.CanAutoMerge()
    95  	})
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	for _, pr := range pulls {
   101  		addToQueue(pr, sha)
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
   108  func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
   109  	if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
   110  		return
   111  	}
   112  
   113  	if err := pull.LoadBaseRepo(ctx); err != nil {
   114  		log.Error("LoadBaseRepo: %v", err)
   115  		return
   116  	}
   117  
   118  	gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
   119  	if err != nil {
   120  		log.Error("OpenRepository: %v", err)
   121  		return
   122  	}
   123  	defer gitRepo.Close()
   124  	commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
   125  	if err != nil {
   126  		log.Error("GetRefCommitID: %v", err)
   127  		return
   128  	}
   129  
   130  	addToQueue(pull, commitID)
   131  }
   132  
   133  func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
   134  	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	defer gitRepo.Close()
   139  
   140  	refs, err := gitRepo.GetRefsBySha(sha, "")
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	pulls := make(map[int64]*issues_model.PullRequest)
   146  
   147  	for _, ref := range refs {
   148  		// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
   149  		// use that to get the pr.
   150  		if strings.HasPrefix(ref, git.PullPrefix) {
   151  			parts := strings.Split(ref[len(git.PullPrefix):], "/")
   152  
   153  			// e.g. 'refs/pull/1/head' would be []string{"1", "head"}
   154  			if len(parts) != 2 {
   155  				log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
   156  				continue
   157  			}
   158  
   159  			prIndex, err := strconv.ParseInt(parts[0], 10, 64)
   160  			if err != nil {
   161  				log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
   162  				continue
   163  			}
   164  
   165  			p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
   166  			if err != nil {
   167  				// If there is no pull request for this branch, we don't try to merge it.
   168  				if issues_model.IsErrPullRequestNotExist(err) {
   169  					continue
   170  				}
   171  				return nil, err
   172  			}
   173  
   174  			if filter(p) {
   175  				pulls[p.ID] = p
   176  			}
   177  		}
   178  	}
   179  
   180  	return pulls, nil
   181  }
   182  
   183  // handlePullRequestAutoMerge merge the pull request if all checks are successful
   184  func handlePullRequestAutoMerge(pullID int64, sha string) {
   185  	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
   186  		fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
   187  	defer finished()
   188  
   189  	pr, err := issues_model.GetPullRequestByID(ctx, pullID)
   190  	if err != nil {
   191  		log.Error("GetPullRequestByID[%d]: %v", pullID, err)
   192  		return
   193  	}
   194  
   195  	// Check if there is a scheduled pr in the db
   196  	exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID)
   197  	if err != nil {
   198  		log.Error("%-v GetScheduledMergeByPullID: %v", pr, err)
   199  		return
   200  	}
   201  	if !exists {
   202  		return
   203  	}
   204  
   205  	if err = pr.LoadBaseRepo(ctx); err != nil {
   206  		log.Error("%-v LoadBaseRepo: %v", pr, err)
   207  		return
   208  	}
   209  
   210  	// check the sha is the same as pull request head commit id
   211  	baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
   212  	if err != nil {
   213  		log.Error("OpenRepository: %v", err)
   214  		return
   215  	}
   216  	defer baseGitRepo.Close()
   217  
   218  	headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   219  	if err != nil {
   220  		log.Error("GetRefCommitID: %v", err)
   221  		return
   222  	}
   223  	if headCommitID != sha {
   224  		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)
   225  		return
   226  	}
   227  
   228  	// Get all checks for this pr
   229  	// We get the latest sha commit hash again to handle the case where the check of a previous push
   230  	// did not succeed or was not finished yet.
   231  	if err = pr.LoadHeadRepo(ctx); err != nil {
   232  		log.Error("%-v LoadHeadRepo: %v", pr, err)
   233  		return
   234  	}
   235  
   236  	var headGitRepo *git.Repository
   237  	if pr.BaseRepoID == pr.HeadRepoID {
   238  		headGitRepo = baseGitRepo
   239  	} else {
   240  		headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
   241  		if err != nil {
   242  			log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
   243  			return
   244  		}
   245  		defer headGitRepo.Close()
   246  	}
   247  
   248  	switch pr.Flow {
   249  	case issues_model.PullRequestFlowGithub:
   250  		headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)
   251  		if pr.HeadRepo == nil || !headBranchExist {
   252  			log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
   253  			return
   254  		}
   255  	case issues_model.PullRequestFlowAGit:
   256  		headBranchExist := git.IsReferenceExist(ctx, baseGitRepo.Path, pr.GetGitRefName())
   257  		if !headBranchExist {
   258  			log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]", pr, pr.HeadRepoID, pr.HeadBranch)
   259  			return
   260  		}
   261  	default:
   262  		log.Error("wrong flow type %d", pr.Flow)
   263  		return
   264  	}
   265  
   266  	// Check if all checks succeeded
   267  	pass, err := pull_service.IsPullCommitStatusPass(ctx, pr)
   268  	if err != nil {
   269  		log.Error("%-v IsPullCommitStatusPass: %v", pr, err)
   270  		return
   271  	}
   272  	if !pass {
   273  		log.Info("Scheduled auto merge %-v has unsuccessful status checks", pr)
   274  		return
   275  	}
   276  
   277  	// Merge if all checks succeeded
   278  	doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
   279  	if err != nil {
   280  		log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err)
   281  		return
   282  	}
   283  
   284  	perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
   285  	if err != nil {
   286  		log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err)
   287  		return
   288  	}
   289  
   290  	if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
   291  		if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) {
   292  			log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
   293  			return
   294  		}
   295  		log.Error("%-v CheckPullMergeable: %v", pr, err)
   296  		return
   297  	}
   298  
   299  	if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
   300  		log.Error("pull_service.Merge: %v", err)
   301  		// FIXME: if merge failed, we should display some error message to the pull request page.
   302  		// The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
   303  		// on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
   304  		return
   305  	}
   306  }