code.gitea.io/gitea@v1.21.7/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/graceful"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/process"
    23  	"code.gitea.io/gitea/modules/queue"
    24  	pull_service "code.gitea.io/gitea/services/pull"
    25  )
    26  
    27  // prAutoMergeQueue represents a queue to handle update pull request tests
    28  var prAutoMergeQueue *queue.WorkerPoolQueue[string]
    29  
    30  // Init runs the task queue to that handles auto merges
    31  func Init() error {
    32  	prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
    33  	if prAutoMergeQueue == nil {
    34  		return fmt.Errorf("unable to create pr_auto_merge queue")
    35  	}
    36  	go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
    37  	return nil
    38  }
    39  
    40  // handle passed PR IDs and test the PRs
    41  func handler(items ...string) []string {
    42  	for _, s := range items {
    43  		var id int64
    44  		var sha string
    45  		if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil {
    46  			log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
    47  			continue
    48  		}
    49  		handlePull(id, sha)
    50  	}
    51  	return nil
    52  }
    53  
    54  func addToQueue(pr *issues_model.PullRequest, sha string) {
    55  	log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
    56  	if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
    57  		log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
    58  	}
    59  }
    60  
    61  // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
    62  func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
    63  	err = db.WithTx(ctx, func(ctx context.Context) error {
    64  		lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
    65  		if err != nil {
    66  			return err
    67  		}
    68  
    69  		// we don't need to schedule
    70  		if lastCommitStatus.IsSuccess() {
    71  			return nil
    72  		}
    73  
    74  		if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
    75  			return err
    76  		}
    77  		scheduled = true
    78  
    79  		_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
    80  		return err
    81  	})
    82  	return scheduled, err
    83  }
    84  
    85  // RemoveScheduledAutoMerge cancels a previously scheduled pull request
    86  func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
    87  	return db.WithTx(ctx, func(ctx context.Context) error {
    88  		if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil {
    89  			return err
    90  		}
    91  
    92  		_, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer)
    93  		return err
    94  	})
    95  }
    96  
    97  // MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
    98  func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
    99  	pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
   100  		return !pr.HasMerged && pr.CanAutoMerge()
   101  	})
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	for _, pr := range pulls {
   107  		addToQueue(pr, sha)
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
   114  	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	defer gitRepo.Close()
   119  
   120  	refs, err := gitRepo.GetRefsBySha(sha, "")
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	pulls := make(map[int64]*issues_model.PullRequest)
   126  
   127  	for _, ref := range refs {
   128  		// Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
   129  		// use that to get the pr.
   130  		if strings.HasPrefix(ref, git.PullPrefix) {
   131  			parts := strings.Split(ref[len(git.PullPrefix):], "/")
   132  
   133  			// e.g. 'refs/pull/1/head' would be []string{"1", "head"}
   134  			if len(parts) != 2 {
   135  				log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
   136  				continue
   137  			}
   138  
   139  			prIndex, err := strconv.ParseInt(parts[0], 10, 64)
   140  			if err != nil {
   141  				log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
   142  				continue
   143  			}
   144  
   145  			p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
   146  			if err != nil {
   147  				// If there is no pull request for this branch, we don't try to merge it.
   148  				if issues_model.IsErrPullRequestNotExist(err) {
   149  					continue
   150  				}
   151  				return nil, err
   152  			}
   153  
   154  			if filter(p) {
   155  				pulls[p.ID] = p
   156  			}
   157  		}
   158  	}
   159  
   160  	return pulls, nil
   161  }
   162  
   163  func handlePull(pullID int64, sha string) {
   164  	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
   165  		fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
   166  	defer finished()
   167  
   168  	pr, err := issues_model.GetPullRequestByID(ctx, pullID)
   169  	if err != nil {
   170  		log.Error("GetPullRequestByID[%d]: %v", pullID, err)
   171  		return
   172  	}
   173  
   174  	// Check if there is a scheduled pr in the db
   175  	exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID)
   176  	if err != nil {
   177  		log.Error("%-v GetScheduledMergeByPullID: %v", pr, err)
   178  		return
   179  	}
   180  	if !exists {
   181  		return
   182  	}
   183  
   184  	// Get all checks for this pr
   185  	// We get the latest sha commit hash again to handle the case where the check of a previous push
   186  	// did not succeed or was not finished yet.
   187  
   188  	if err = pr.LoadHeadRepo(ctx); err != nil {
   189  		log.Error("%-v LoadHeadRepo: %v", pr, err)
   190  		return
   191  	}
   192  
   193  	headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
   194  	if err != nil {
   195  		log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
   196  		return
   197  	}
   198  	defer headGitRepo.Close()
   199  
   200  	headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)
   201  
   202  	if pr.HeadRepo == nil || !headBranchExist {
   203  		log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
   204  		return
   205  	}
   206  
   207  	// Check if all checks succeeded
   208  	pass, err := pull_service.IsPullCommitStatusPass(ctx, pr)
   209  	if err != nil {
   210  		log.Error("%-v IsPullCommitStatusPass: %v", pr, err)
   211  		return
   212  	}
   213  	if !pass {
   214  		log.Info("Scheduled auto merge %-v has unsuccessful status checks", pr)
   215  		return
   216  	}
   217  
   218  	// Merge if all checks succeeded
   219  	doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
   220  	if err != nil {
   221  		log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err)
   222  		return
   223  	}
   224  
   225  	perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
   226  	if err != nil {
   227  		log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err)
   228  		return
   229  	}
   230  
   231  	if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
   232  		if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) {
   233  			log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
   234  			return
   235  		}
   236  		log.Error("%-v CheckPullMergable: %v", pr, err)
   237  		return
   238  	}
   239  
   240  	var baseGitRepo *git.Repository
   241  	if pr.BaseRepoID == pr.HeadRepoID {
   242  		baseGitRepo = headGitRepo
   243  	} else {
   244  		if err = pr.LoadBaseRepo(ctx); err != nil {
   245  			log.Error("%-v LoadBaseRepo: %v", pr, err)
   246  			return
   247  		}
   248  
   249  		baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
   250  		if err != nil {
   251  			log.Error("OpenRepository %-v: %v", pr.BaseRepo, err)
   252  			return
   253  		}
   254  		defer baseGitRepo.Close()
   255  	}
   256  
   257  	if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
   258  		log.Error("pull_service.Merge: %v", err)
   259  		return
   260  	}
   261  }