2019-06-22 18:35:34 +01:00
// Copyright 2019 The Gitea Authors.
// All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-06-22 18:35:34 +01:00
package pull
import (
2022-01-19 23:26:57 +00:00
"context"
2025-04-01 12:14:01 +02:00
"errors"
2019-06-22 18:35:34 +01:00
"fmt"
2025-06-18 03:48:09 +02:00
"maps"
2019-06-22 18:35:34 +01:00
"os"
"path/filepath"
2019-11-01 03:30:02 +03:00
"regexp"
2022-05-08 20:32:45 +08:00
"strconv"
2019-06-22 18:35:34 +01:00
"strings"
2025-06-02 14:29:16 +08:00
"unicode"
2019-06-22 18:35:34 +01:00
2022-05-03 21:46:28 +02:00
"code.gitea.io/gitea/models/db"
2022-06-12 23:51:54 +08: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"
2025-01-07 19:16:56 -08:00
pull_model "code.gitea.io/gitea/models/pull"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2019-06-22 18:35:34 +01:00
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
2025-09-15 23:33:12 -07:00
"code.gitea.io/gitea/modules/git/gitcmd"
2024-09-06 18:12:41 +08:00
"code.gitea.io/gitea/modules/globallock"
2024-06-15 11:43:57 +08:00
"code.gitea.io/gitea/modules/httplib"
2019-06-22 18:35:34 +01:00
"code.gitea.io/gitea/modules/log"
2019-11-18 10:13:07 -03:00
"code.gitea.io/gitea/modules/references"
2022-05-09 00:46:32 +08:00
repo_module "code.gitea.io/gitea/modules/repository"
2019-06-22 18:35:34 +01:00
"code.gitea.io/gitea/modules/setting"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2024-12-20 10:05:29 -08:00
"code.gitea.io/gitea/modules/util"
2019-11-18 10:13:07 -03:00
issue_service "code.gitea.io/gitea/services/issue"
2023-09-06 02:37:47 +08:00
notify_service "code.gitea.io/gitea/services/notify"
2019-06-22 18:35:34 +01:00
)
2023-05-22 03:01:46 +02:00
// getMergeMessage composes the message used when merging a pull request.
func getMergeMessage ( ctx context . Context , baseGitRepo * git . Repository , pr * issues_model . PullRequest , mergeStyle repo_model . MergeStyle , extraVars map [ string ] string ) ( message , body string , err error ) {
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2022-12-29 20:40:20 +08:00
return "" , "" , err
2022-05-08 20:32:45 +08:00
}
2023-03-07 20:07:35 +00:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
return "" , "" , err
2022-05-08 20:32:45 +08:00
}
2022-11-19 09:12:33 +01:00
if err := pr . LoadIssue ( ctx ) ; err != nil {
2022-12-29 20:40:20 +08:00
return "" , "" , err
2022-05-08 20:32:45 +08:00
}
2023-10-17 23:07:23 +08:00
if err := pr . Issue . LoadPoster ( ctx ) ; err != nil {
return "" , "" , err
}
2024-06-06 10:35:04 +02:00
if err := pr . Issue . LoadRepo ( ctx ) ; err != nil {
return "" , "" , err
}
2022-05-08 20:32:45 +08:00
2022-12-10 10:46:31 +08:00
isExternalTracker := pr . BaseRepo . UnitEnabled ( ctx , unit . TypeExternalTracker )
2022-05-08 20:32:45 +08:00
issueReference := "#"
if isExternalTracker {
issueReference = "!"
}
2025-04-01 12:14:01 +02:00
reviewedOn := "Reviewed-on: " + httplib . MakeAbsoluteURL ( ctx , pr . Issue . Link ( ) )
2024-06-06 10:35:04 +02:00
reviewedBy := pr . GetApprovers ( ctx )
2022-05-08 20:32:45 +08:00
if mergeStyle != "" {
templateFilepath := fmt . Sprintf ( ".gitea/default_merge_message/%s_TEMPLATE.md" , strings . ToUpper ( string ( mergeStyle ) ) )
commit , err := baseGitRepo . GetBranchCommit ( pr . BaseRepo . DefaultBranch )
if err != nil {
2022-12-29 20:40:20 +08:00
return "" , "" , err
2022-05-08 20:32:45 +08:00
}
templateContent , err := commit . GetFileContent ( templateFilepath , setting . Repository . PullRequest . DefaultMergeMessageSize )
if err != nil {
if ! git . IsErrNotExist ( err ) {
2022-12-29 20:40:20 +08:00
return "" , "" , err
2022-05-08 20:32:45 +08:00
}
} else {
vars := map [ string ] string {
"BaseRepoOwnerName" : pr . BaseRepo . OwnerName ,
"BaseRepoName" : pr . BaseRepo . Name ,
"BaseBranch" : pr . BaseBranch ,
"HeadRepoOwnerName" : "" ,
"HeadRepoName" : "" ,
"HeadBranch" : pr . HeadBranch ,
"PullRequestTitle" : pr . Issue . Title ,
"PullRequestDescription" : pr . Issue . Content ,
"PullRequestPosterName" : pr . Issue . Poster . Name ,
"PullRequestIndex" : strconv . FormatInt ( pr . Index , 10 ) ,
"PullRequestReference" : fmt . Sprintf ( "%s%d" , issueReference , pr . Index ) ,
2024-06-06 10:35:04 +02:00
"ReviewedOn" : reviewedOn ,
"ReviewedBy" : reviewedBy ,
2022-05-08 20:32:45 +08:00
}
if pr . HeadRepo != nil {
vars [ "HeadRepoOwnerName" ] = pr . HeadRepo . OwnerName
vars [ "HeadRepoName" ] = pr . HeadRepo . Name
}
2025-06-18 03:48:09 +02:00
maps . Copy ( vars , extraVars )
2022-11-19 09:12:33 +01:00
refs , err := pr . ResolveCrossReferences ( ctx )
2022-05-08 20:32:45 +08:00
if err == nil {
closeIssueIndexes := make ( [ ] string , 0 , len ( refs ) )
closeWord := "close"
if len ( setting . Repository . PullRequest . CloseKeywords ) > 0 {
closeWord = setting . Repository . PullRequest . CloseKeywords [ 0 ]
}
for _ , ref := range refs {
if ref . RefAction == references . XRefActionCloses {
2023-02-08 20:47:52 -06:00
if err := ref . LoadIssue ( ctx ) ; err != nil {
return "" , "" , err
}
2022-05-08 20:32:45 +08:00
closeIssueIndexes = append ( closeIssueIndexes , fmt . Sprintf ( "%s %s%d" , closeWord , issueReference , ref . Issue . Index ) )
}
}
if len ( closeIssueIndexes ) > 0 {
vars [ "ClosingIssues" ] = strings . Join ( closeIssueIndexes , ", " )
} else {
vars [ "ClosingIssues" ] = ""
}
}
2022-12-29 20:40:20 +08:00
message , body = expandDefaultMergeMessage ( templateContent , vars )
return message , body , nil
2022-05-08 20:32:45 +08:00
}
}
2023-07-10 16:12:50 +08:00
if mergeStyle == repo_model . MergeStyleRebase {
// for fast-forward rebase, do not amend the last commit if there is no template
return "" , "" , nil
}
2024-06-06 10:35:04 +02:00
body = fmt . Sprintf ( "%s\n%s" , reviewedOn , reviewedBy )
2022-05-08 20:32:45 +08:00
// Squash merge has a different from other styles.
if mergeStyle == repo_model . MergeStyleSquash {
2024-06-06 10:35:04 +02:00
return fmt . Sprintf ( "%s (%s%d)" , pr . Issue . Title , issueReference , pr . Issue . Index ) , body , nil
2022-05-08 20:32:45 +08:00
}
if pr . BaseRepoID == pr . HeadRepoID {
2024-06-06 10:35:04 +02:00
return fmt . Sprintf ( "Merge pull request '%s' (%s%d) from %s into %s" , pr . Issue . Title , issueReference , pr . Issue . Index , pr . HeadBranch , pr . BaseBranch ) , body , nil
2022-05-08 20:32:45 +08:00
}
if pr . HeadRepo == nil {
2024-06-06 10:35:04 +02:00
return fmt . Sprintf ( "Merge pull request '%s' (%s%d) from <deleted>:%s into %s" , pr . Issue . Title , issueReference , pr . Issue . Index , pr . HeadBranch , pr . BaseBranch ) , body , nil
2022-05-08 20:32:45 +08:00
}
2024-06-06 10:35:04 +02:00
return fmt . Sprintf ( "Merge pull request '%s' (%s%d) from %s:%s into %s" , pr . Issue . Title , issueReference , pr . Issue . Index , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseBranch ) , body , nil
2022-12-29 20:40:20 +08:00
}
func expandDefaultMergeMessage ( template string , vars map [ string ] string ) ( message , body string ) {
message = strings . TrimSpace ( template )
if splits := strings . SplitN ( message , "\n" , 2 ) ; len ( splits ) == 2 {
message = splits [ 0 ]
body = strings . TrimSpace ( splits [ 1 ] )
}
mapping := func ( s string ) string { return vars [ s ] }
return os . Expand ( message , mapping ) , os . Expand ( body , mapping )
2022-05-08 20:32:45 +08:00
}
2023-05-22 03:01:46 +02:00
// GetDefaultMergeMessage returns default message used when merging pull request
func GetDefaultMergeMessage ( ctx context . Context , baseGitRepo * git . Repository , pr * issues_model . PullRequest , mergeStyle repo_model . MergeStyle ) ( message , body string , err error ) {
return getMergeMessage ( ctx , baseGitRepo , pr , mergeStyle , nil )
}
2025-06-02 14:29:16 +08:00
func AddCommitMessageTailer ( message , tailerKey , tailerValue string ) string {
tailerLine := tailerKey + ": " + tailerValue
message = strings . ReplaceAll ( message , "\r\n" , "\n" )
message = strings . ReplaceAll ( message , "\r" , "\n" )
if strings . Contains ( message , "\n" + tailerLine + "\n" ) || strings . HasSuffix ( message , "\n" + tailerLine ) {
return message
}
if ! strings . HasSuffix ( message , "\n" ) {
message += "\n"
}
pos1 := strings . LastIndexByte ( message [ : len ( message ) - 1 ] , '\n' )
pos2 := - 1
if pos1 != - 1 {
pos2 = strings . IndexByte ( message [ pos1 : ] , ':' )
if pos2 != - 1 {
pos2 += pos1
}
}
var lastLineKey string
if pos1 != - 1 && pos2 != - 1 {
lastLineKey = message [ pos1 + 1 : pos2 ]
}
isLikelyTailerLine := lastLineKey != "" && unicode . IsUpper ( rune ( lastLineKey [ 0 ] ) ) && strings . Contains ( message , "-" )
for i := 0 ; isLikelyTailerLine && i < len ( lastLineKey ) ; i ++ {
r := rune ( lastLineKey [ i ] )
isLikelyTailerLine = unicode . IsLetter ( r ) || unicode . IsDigit ( r ) || r == '-'
}
if ! strings . HasSuffix ( message , "\n\n" ) && ! isLikelyTailerLine {
message += "\n"
}
return message + tailerLine
}
2024-12-20 10:05:29 -08:00
// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
type ErrInvalidMergeStyle struct {
ID int64
Style repo_model . MergeStyle
}
// IsErrInvalidMergeStyle checks if an error is a ErrInvalidMergeStyle.
func IsErrInvalidMergeStyle ( err error ) bool {
_ , ok := err . ( ErrInvalidMergeStyle )
return ok
}
func ( err ErrInvalidMergeStyle ) Error ( ) string {
return fmt . Sprintf ( "merge strategy is not allowed or is invalid [repo_id: %d, strategy: %s]" ,
err . ID , err . Style )
}
func ( err ErrInvalidMergeStyle ) Unwrap ( ) error {
return util . ErrInvalidArgument
}
2019-06-22 18:35:34 +01:00
// Merge merges pull request to base repository.
2020-01-11 08:29:34 +01:00
// Caller should check PR is ready to be merged (review and status checks)
2025-10-31 21:56:08 -07:00
func Merge ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User , mergeStyle repo_model . MergeStyle , expectedHeadCommitID , message string , wasAutoMerged bool ) error {
2023-03-07 20:07:35 +00:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
log . Error ( "Unable to load base repo: %v" , err )
return fmt . Errorf ( "unable to load base repo: %w" , err )
} else if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
log . Error ( "Unable to load head repo: %v" , err )
return fmt . Errorf ( "unable to load head repo: %w" , err )
2019-06-22 18:35:34 +01:00
}
2022-12-10 10:46:31 +08:00
prUnit , err := pr . BaseRepo . GetUnit ( ctx , unit . TypePullRequests )
2019-06-22 18:35:34 +01:00
if err != nil {
2021-11-10 03:57:58 +08:00
log . Error ( "pr.BaseRepo.GetUnit(unit.TypePullRequests): %v" , err )
2019-06-22 18:35:34 +01:00
return err
}
prConfig := prUnit . PullRequestsConfig ( )
// Check if merge style is correct and allowed
if ! prConfig . IsMergeStyleAllowed ( mergeStyle ) {
2024-12-20 10:05:29 -08:00
return ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : mergeStyle }
2019-06-22 18:35:34 +01:00
}
2024-09-06 18:12:41 +08:00
releaser , err := globallock . Lock ( ctx , getPullWorkingLockKey ( pr . ID ) )
if err != nil {
log . Error ( "lock.Lock(): %v" , err )
return fmt . Errorf ( "lock.Lock: %w" , err )
}
defer releaser ( )
2019-06-22 18:35:34 +01:00
defer func ( ) {
2025-10-14 12:19:27 -07:00
// This is a duplicated call to AddTestPullRequestTask (it will also be called by the post-receive hook, via a push queue).
// This call will do some operations (push to base repo, sync commit divergence, add PR conflict check queue task, etc)
// immediately instead of waiting for the "push queue"'s task. The code is from https://github.com/go-gitea/gitea/pull/7082.
// But it's really questionable whether it's worth to do it ahead without waiting for the "push queue" task to run.
// TODO: DUPLICATE-PR-TASK: maybe can try to remove this in 1.26 to see if there is any issue.
2025-03-13 19:36:14 -07:00
go AddTestPullRequestTask ( TestPullRequestOptions {
RepoID : pr . BaseRepo . ID ,
Doer : doer ,
Branch : pr . BaseBranch ,
IsSync : false ,
IsForcePush : false ,
OldCommitID : "" ,
NewCommitID : "" ,
} )
2019-06-22 18:35:34 +01:00
} ( )
2024-05-07 15:36:48 +08:00
_ , err = doMergeAndPush ( ctx , pr , doer , mergeStyle , expectedHeadCommitID , message , repo_module . PushTriggerPRMergeToBase )
2024-09-06 18:12:41 +08:00
releaser ( )
2020-01-17 07:03:40 +01:00
if err != nil {
2020-02-09 23:09:31 +00:00
return err
2020-01-17 07:03:40 +01:00
}
2024-05-07 15:36:48 +08:00
// reload pull request because it has been updated by post receive hook
pr , err = issues_model . GetPullRequestByID ( ctx , pr . ID )
if err != nil {
return err
2020-01-17 07:03:40 +01:00
}
2023-07-22 22:14:27 +08:00
if err := pr . LoadIssue ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "LoadIssue %-v: %v" , pr , err )
2020-02-09 23:09:31 +00:00
}
2023-07-22 22:14:27 +08:00
if err := pr . Issue . LoadRepo ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "pr.Issue.LoadRepo %-v: %v" , pr , err )
2020-02-09 23:09:31 +00:00
}
2023-07-22 22:14:27 +08:00
if err := pr . Issue . Repo . LoadOwner ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "LoadOwner for %-v: %v" , pr , err )
2020-02-09 23:09:31 +00:00
}
2022-11-03 16:49:00 +01:00
if wasAutoMerged {
2023-09-06 02:37:47 +08:00
notify_service . AutoMergePullRequest ( ctx , doer , pr )
2022-11-03 16:49:00 +01:00
} else {
2023-09-06 02:37:47 +08:00
notify_service . MergePullRequest ( ctx , doer , pr )
2022-11-03 16:49:00 +01:00
}
2020-01-17 07:03:40 +01:00
// Reset cached commit count
cache . Remove ( pr . Issue . Repo . GetCommitsCountCacheKey ( pr . BaseBranch , true ) )
2024-08-26 01:18:19 +08:00
return handleCloseCrossReferences ( ctx , pr , doer )
}
func handleCloseCrossReferences ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User ) error {
2020-01-17 07:03:40 +01:00
// Resolve cross references
2023-07-22 22:14:27 +08:00
refs , err := pr . ResolveCrossReferences ( ctx )
2020-01-17 07:03:40 +01:00
if err != nil {
log . Error ( "ResolveCrossReferences: %v" , err )
return nil
}
for _ , ref := range refs {
2023-07-22 22:14:27 +08:00
if err = ref . LoadIssue ( ctx ) ; err != nil {
2020-01-17 07:03:40 +01:00
return err
}
2023-07-22 22:14:27 +08:00
if err = ref . Issue . LoadRepo ( ctx ) ; err != nil {
2020-01-17 07:03:40 +01:00
return err
}
2024-12-24 23:38:30 -08:00
if ref . RefAction == references . XRefActionCloses && ! ref . Issue . IsClosed {
if err = issue_service . CloseIssue ( ctx , ref . Issue , doer , pr . MergedCommitID ) ; err != nil {
2022-01-18 23:26:42 +00:00
// Allow ErrDependenciesLeft
2022-06-13 17:37:59 +08:00
if ! issues_model . IsErrDependenciesLeft ( err ) {
2022-01-18 23:26:42 +00:00
return err
}
2020-01-17 07:03:40 +01:00
}
2024-12-24 23:38:30 -08:00
} else if ref . RefAction == references . XRefActionReopens && ref . Issue . IsClosed {
if err = issue_service . ReopenIssue ( ctx , ref . Issue , doer , pr . MergedCommitID ) ; err != nil {
return err
}
2020-01-17 07:03:40 +01:00
}
}
return nil
}
2023-03-07 20:07:35 +00:00
// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository
2025-06-27 15:48:03 +02:00
func doMergeAndPush ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User , mergeStyle repo_model . MergeStyle , expectedHeadCommitID , message string , pushTrigger repo_module . PushTrigger ) ( string , error ) { //nolint:unparam // non-error result is never used
2019-06-22 18:35:34 +01:00
// Clone base repo.
2023-03-07 20:07:35 +00:00
mergeCtx , cancel , err := createTemporaryRepoForMerge ( ctx , pr , doer , expectedHeadCommitID )
2019-06-22 18:35:34 +01:00
if err != nil {
2020-02-09 23:09:31 +00:00
return "" , err
2019-06-22 18:35:34 +01:00
}
2023-03-07 20:07:35 +00:00
defer cancel ( )
2019-10-16 14:42:42 +01:00
2019-06-22 18:35:34 +01:00
// Merge commits.
switch mergeStyle {
2021-12-10 09:27:50 +08:00
case repo_model . MergeStyleMerge :
2023-03-07 20:07:35 +00:00
if err := doMergeStyleMerge ( mergeCtx , message ) ; err != nil {
2020-02-09 23:09:31 +00:00
return "" , err
2019-06-22 18:35:34 +01:00
}
2023-03-07 20:07:35 +00:00
case repo_model . MergeStyleRebase , repo_model . MergeStyleRebaseMerge :
if err := doMergeStyleRebase ( mergeCtx , mergeStyle , message ) ; err != nil {
2020-02-09 23:09:31 +00:00
return "" , err
2019-06-22 18:35:34 +01:00
}
2021-12-10 09:27:50 +08:00
case repo_model . MergeStyleSquash :
2023-03-07 20:07:35 +00:00
if err := doMergeStyleSquash ( mergeCtx , message ) ; err != nil {
2020-02-09 23:09:31 +00:00
return "" , err
2019-06-22 18:35:34 +01:00
}
2024-02-12 14:37:23 -08:00
case repo_model . MergeStyleFastForwardOnly :
if err := doMergeStyleFastForwardOnly ( mergeCtx ) ; err != nil {
return "" , err
}
2019-06-22 18:35:34 +01:00
default :
2024-12-20 10:05:29 -08:00
return "" , ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : mergeStyle }
2019-06-22 18:35:34 +01:00
}
// OK we should cache our current head and origin/headbranch
2023-03-07 20:07:35 +00:00
mergeHeadSHA , err := git . GetFullCommitID ( ctx , mergeCtx . tmpBasePath , "HEAD" )
2019-06-22 18:35:34 +01:00
if err != nil {
2022-10-24 21:29:17 +02:00
return "" , fmt . Errorf ( "Failed to get full commit id for HEAD: %w" , err )
2019-06-22 18:35:34 +01:00
}
2026-01-27 11:57:20 -08:00
mergeBaseSHA , err := git . GetFullCommitID ( ctx , mergeCtx . tmpBasePath , "original_" + tmpRepoBaseBranch )
2019-06-22 18:35:34 +01:00
if err != nil {
2022-10-24 21:29:17 +02:00
return "" , fmt . Errorf ( "Failed to get full commit id for origin/%s: %w" , pr . BaseBranch , err )
2020-02-09 23:09:31 +00:00
}
2026-01-27 11:57:20 -08:00
mergeCommitID , err := git . GetFullCommitID ( ctx , mergeCtx . tmpBasePath , tmpRepoBaseBranch )
2020-02-09 23:09:31 +00:00
if err != nil {
2022-10-24 21:29:17 +02:00
return "" , fmt . Errorf ( "Failed to get full commit id for the new merge: %w" , err )
2019-06-22 18:35:34 +01:00
}
// Now it's questionable about where this should go - either after or before the push
// I think in the interests of data safety - failures to push to the lfs should prevent
// the merge as you can always remerge.
if setting . LFS . StartServer {
2023-03-07 20:07:35 +00:00
if err := LFSPush ( ctx , mergeCtx . tmpBasePath , mergeHeadSHA , mergeBaseSHA , pr ) ; err != nil {
2020-02-09 23:09:31 +00:00
return "" , err
2019-06-22 18:35:34 +01:00
}
}
2021-11-24 17:49:20 +08:00
var headUser * user_model . User
2023-02-18 21:11:03 +09:00
err = pr . HeadRepo . LoadOwner ( ctx )
2019-07-01 02:18:13 +01:00
if err != nil {
2021-11-24 17:49:20 +08:00
if ! user_model . IsErrUserNotExist ( err ) {
2023-03-07 20:07:35 +00:00
log . Error ( "Can't find user: %d for head repository in %-v: %v" , pr . HeadRepo . OwnerID , pr , err )
2020-02-09 23:09:31 +00:00
return "" , err
2019-07-01 02:18:13 +01:00
}
2023-03-07 20:07:35 +00:00
log . Warn ( "Can't find user: %d for head repository in %-v - defaulting to doer: %s - %v" , pr . HeadRepo . OwnerID , pr , doer . Name , err )
2019-07-01 02:18:13 +01:00
headUser = doer
2019-10-18 19:13:31 +08:00
} else {
headUser = pr . HeadRepo . Owner
2019-07-01 02:18:13 +01:00
}
2023-03-07 20:07:35 +00:00
mergeCtx . env = repo_module . FullPushingEnvironment (
headUser ,
doer ,
pr . BaseRepo ,
pr . BaseRepo . Name ,
pr . ID ,
2025-11-14 05:21:05 +01:00
pr . Index ,
2023-03-07 20:07:35 +00:00
)
2024-05-07 15:36:48 +08:00
mergeCtx . env = append ( mergeCtx . env , repo_module . EnvPushTrigger + "=" + string ( pushTrigger ) )
2026-01-27 11:57:20 -08:00
pushCmd := gitcmd . NewCommand ( "push" , "origin" ) . AddDynamicArguments ( tmpRepoBaseBranch + ":" + git . BranchPrefix + pr . BaseBranch )
2021-08-31 22:03:45 +08:00
2019-06-22 18:35:34 +01:00
// Push back to upstream.
2024-05-07 15:36:48 +08:00
// This cause an api call to "/api/internal/hook/post-receive/...",
// If it's merge, all db transaction and operations should be there but not here to prevent deadlock.
2026-01-19 07:10:33 +08:00
if err := mergeCtx . PrepareGitCmd ( pushCmd ) . RunWithStderr ( ctx ) ; err != nil {
if strings . Contains ( err . Stderr ( ) , "non-fast-forward" ) {
2020-03-28 04:13:18 +00:00
return "" , & git . ErrPushOutOfDate {
2023-03-07 20:07:35 +00:00
StdOut : mergeCtx . outbuf . String ( ) ,
2026-01-19 07:10:33 +08:00
StdErr : err . Stderr ( ) ,
2019-11-10 08:42:51 +00:00
Err : err ,
}
2026-01-19 07:10:33 +08:00
} else if strings . Contains ( err . Stderr ( ) , "! [remote rejected]" ) {
2020-03-28 04:13:18 +00:00
err := & git . ErrPushRejected {
2023-03-07 20:07:35 +00:00
StdOut : mergeCtx . outbuf . String ( ) ,
2026-01-19 07:10:33 +08:00
StdErr : err . Stderr ( ) ,
2020-02-22 13:08:48 +00:00
Err : err ,
}
err . GenerateMessage ( )
return "" , err
2019-11-10 08:42:51 +00:00
}
2026-01-19 07:10:33 +08:00
return "" , fmt . Errorf ( "git push: %s" , err . Stderr ( ) )
2019-06-22 18:35:34 +01:00
}
2023-03-07 20:07:35 +00:00
mergeCtx . outbuf . Reset ( )
2020-02-09 23:09:31 +00:00
return mergeCommitID , nil
2019-06-22 18:35:34 +01:00
}
2023-03-07 20:07:35 +00:00
func commitAndSignNoAuthor ( ctx * mergeContext , message string ) error {
2025-09-15 23:33:12 -07:00
cmdCommit := gitcmd . NewCommand ( "commit" ) . AddOptionFormat ( "--message=%s" , message )
2026-04-05 13:37:35 -07:00
addCommitSigningOptions ( cmdCommit , ctx . signKey )
2026-01-19 07:10:33 +08:00
if err := ctx . PrepareGitCmd ( cmdCommit ) . RunWithStderr ( ctx ) ; err != nil {
return fmt . Errorf ( "git commit %v: %w\n%s" , ctx . pr , err , ctx . outbuf . String ( ) )
2019-11-10 08:42:51 +00:00
}
return nil
}
2026-04-05 13:37:35 -07:00
func addCommitSigningOptions ( cmd * gitcmd . Command , signKey * git . SigningKey ) {
if signKey == nil {
cmd . AddArguments ( "--no-gpg-sign" )
return
}
if signKey . Format != "" {
cmd . AddConfig ( "gpg.format" , signKey . Format )
}
cmd . AddOptionFormat ( "--gpg-sign=%s" , signKey . KeyID )
}
2024-12-20 10:05:29 -08:00
// ErrMergeConflicts represents an error if merging fails with a conflict
type ErrMergeConflicts struct {
Style repo_model . MergeStyle
StdOut string
StdErr string
Err error
}
// IsErrMergeConflicts checks if an error is a ErrMergeConflicts.
func IsErrMergeConflicts ( err error ) bool {
_ , ok := err . ( ErrMergeConflicts )
return ok
}
func ( err ErrMergeConflicts ) Error ( ) string {
return fmt . Sprintf ( "Merge Conflict Error: %v: %s\n%s" , err . Err , err . StdErr , err . StdOut )
}
// ErrMergeUnrelatedHistories represents an error if merging fails due to unrelated histories
type ErrMergeUnrelatedHistories struct {
Style repo_model . MergeStyle
StdOut string
StdErr string
Err error
}
// IsErrMergeUnrelatedHistories checks if an error is a ErrMergeUnrelatedHistories.
func IsErrMergeUnrelatedHistories ( err error ) bool {
_ , ok := err . ( ErrMergeUnrelatedHistories )
return ok
}
func ( err ErrMergeUnrelatedHistories ) Error ( ) string {
return fmt . Sprintf ( "Merge UnrelatedHistories Error: %v: %s\n%s" , err . Err , err . StdErr , err . StdOut )
}
// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge
type ErrMergeDivergingFastForwardOnly struct {
StdOut string
StdErr string
Err error
}
// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly.
func IsErrMergeDivergingFastForwardOnly ( err error ) bool {
_ , ok := err . ( ErrMergeDivergingFastForwardOnly )
return ok
}
func ( err ErrMergeDivergingFastForwardOnly ) Error ( ) string {
return fmt . Sprintf ( "Merge DivergingFastForwardOnly Error: %v: %s\n%s" , err . Err , err . StdErr , err . StdOut )
}
2025-09-15 23:33:12 -07:00
func runMergeCommand ( ctx * mergeContext , mergeStyle repo_model . MergeStyle , cmd * gitcmd . Command ) error {
2026-01-19 07:10:33 +08:00
if err := ctx . PrepareGitCmd ( cmd ) . RunWithStderr ( ctx ) ; err != nil {
2019-11-10 08:42:51 +00:00
// Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict
2023-03-07 20:07:35 +00:00
if _ , statErr := os . Stat ( filepath . Join ( ctx . tmpBasePath , ".git" , "MERGE_HEAD" ) ) ; statErr == nil {
2019-11-10 08:42:51 +00:00
// We have a merge conflict error
2026-01-19 07:10:33 +08:00
log . Debug ( "MergeConflict %-v: %v\n%s\n%s" , ctx . pr , err , ctx . outbuf . String ( ) , err . Stderr ( ) )
2024-12-20 10:05:29 -08:00
return ErrMergeConflicts {
2019-11-10 08:42:51 +00:00
Style : mergeStyle ,
2023-03-07 20:07:35 +00:00
StdOut : ctx . outbuf . String ( ) ,
2026-01-19 07:10:33 +08:00
StdErr : err . Stderr ( ) ,
2019-11-10 08:42:51 +00:00
Err : err ,
}
2026-01-19 07:10:33 +08:00
} else if strings . Contains ( err . Stderr ( ) , "refusing to merge unrelated histories" ) {
log . Debug ( "MergeUnrelatedHistories %-v: %v\n%s\n%s" , ctx . pr , err , ctx . outbuf . String ( ) , err . Stderr ( ) )
2024-12-20 10:05:29 -08:00
return ErrMergeUnrelatedHistories {
2019-11-10 08:42:51 +00:00
Style : mergeStyle ,
2023-03-07 20:07:35 +00:00
StdOut : ctx . outbuf . String ( ) ,
2026-01-19 07:10:33 +08:00
StdErr : err . Stderr ( ) ,
2019-11-10 08:42:51 +00:00
Err : err ,
}
2026-01-19 07:10:33 +08:00
} else if mergeStyle == repo_model . MergeStyleFastForwardOnly && strings . Contains ( err . Stderr ( ) , "Not possible to fast-forward, aborting" ) {
log . Debug ( "MergeDivergingFastForwardOnly %-v: %v\n%s\n%s" , ctx . pr , err , ctx . outbuf . String ( ) , err . Stderr ( ) )
2024-12-20 10:05:29 -08:00
return ErrMergeDivergingFastForwardOnly {
2024-02-12 14:37:23 -08:00
StdOut : ctx . outbuf . String ( ) ,
2026-01-19 07:10:33 +08:00
StdErr : err . Stderr ( ) ,
2024-02-12 14:37:23 -08:00
Err : err ,
}
2019-11-10 08:42:51 +00:00
}
2026-01-19 07:10:33 +08:00
log . Error ( "git merge %-v: %v\n%s\n%s" , ctx . pr , err , ctx . outbuf . String ( ) , err . Stderr ( ) )
return fmt . Errorf ( "git merge %v: %w\n%s\n%s" , ctx . pr , err , ctx . outbuf . String ( ) , err . Stderr ( ) )
2019-11-10 08:42:51 +00:00
}
2023-03-07 20:07:35 +00:00
ctx . outbuf . Reset ( )
2019-11-10 08:42:51 +00:00
return nil
}
2019-11-01 03:30:02 +03:00
var escapedSymbols = regexp . MustCompile ( ` ([*[?! \\]) ` )
2020-01-11 08:29:34 +01:00
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
2022-06-13 17:37:59 +08:00
func IsUserAllowedToMerge ( ctx context . Context , pr * issues_model . PullRequest , p access_model . Permission , user * user_model . User ) ( bool , error ) {
2025-11-21 23:16:08 -08:00
return isUserAllowedToMergeInRepoBranch ( ctx , pr . BaseRepoID , pr . BaseBranch , p , user )
}
func isUserAllowedToMergeInRepoBranch ( ctx context . Context , repoID int64 , branch string , p access_model . Permission , user * user_model . User ) ( bool , error ) {
2020-04-14 18:29:31 +02:00
if user == nil {
return false , nil
}
2020-01-11 08:29:34 +01:00
2025-11-21 23:16:08 -08:00
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , repoID , branch )
2020-01-11 08:29:34 +01:00
if err != nil {
return false , err
}
2023-01-16 16:00:22 +08:00
if ( p . CanWrite ( unit . TypeCode ) && pb == nil ) || ( pb != nil && git_model . IsUserMergeWhitelisted ( ctx , pb , user . ID , p ) ) {
2020-01-11 08:29:34 +01:00
return true , nil
}
return false , nil
}
2022-05-02 01:54:44 +02:00
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
2022-06-13 17:37:59 +08:00
func CheckPullBranchProtections ( ctx context . Context , pr * issues_model . PullRequest , skipProtectedFilesCheck bool ) ( err error ) {
2022-11-19 09:12:33 +01:00
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "LoadBaseRepo: %w" , err )
2020-03-03 06:31:55 +08:00
}
2023-01-16 16:00:22 +08:00
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , pr . BaseRepoID , pr . BaseBranch )
if err != nil {
return fmt . Errorf ( "LoadProtectedBranch: %v" , err )
2020-01-11 08:29:34 +01:00
}
2023-01-16 16:00:22 +08:00
if pb == nil {
2020-03-03 06:31:55 +08:00
return nil
2020-01-11 08:29:34 +01:00
}
2022-01-19 23:26:57 +00:00
isPass , err := IsPullCommitStatusPass ( ctx , pr )
2020-01-11 08:29:34 +01:00
if err != nil {
return err
}
if ! isPass {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "Not all required status checks successful" )
2020-01-11 08:29:34 +01:00
}
2023-01-16 16:00:22 +08:00
if ! issues_model . HasEnoughApprovals ( ctx , pb , pr ) {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "Does not have enough approvals" )
2020-01-11 08:29:34 +01:00
}
2023-01-16 16:00:22 +08:00
if issues_model . MergeBlockedByRejectedReview ( ctx , pb , pr ) {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "There are requested changes" )
2020-01-11 08:29:34 +01:00
}
2023-01-16 16:00:22 +08:00
if issues_model . MergeBlockedByOfficialReviewRequests ( ctx , pb , pr ) {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "There are official review requests" )
2020-11-29 03:30:46 +08:00
}
2020-01-11 08:29:34 +01:00
2023-01-16 16:00:22 +08:00
if issues_model . MergeBlockedByOutdatedBranch ( pb , pr ) {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "The head branch is behind the base branch" )
2020-04-17 03:00:36 +02:00
}
2020-10-14 02:50:57 +08:00
if skipProtectedFilesCheck {
return nil
}
2023-01-16 16:00:22 +08:00
if pb . MergeBlockedByProtectedFiles ( pr . ChangedProtectedFiles ) {
2025-04-24 21:26:57 +02:00
return util . ErrorWrap ( ErrNotReadyToMerge , "Changed protected files" )
2020-10-14 02:50:57 +08:00
}
2020-01-11 08:29:34 +01:00
return nil
}
2021-03-04 11:41:23 +08:00
// MergedManually mark pr as merged manually
2023-10-14 10:37:24 +02:00
func MergedManually ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User , baseGitRepo * git . Repository , commitID string ) error {
2024-09-06 18:12:41 +08:00
releaser , err := globallock . Lock ( ctx , getPullWorkingLockKey ( pr . ID ) )
if err != nil {
log . Error ( "lock.Lock(): %v" , err )
return fmt . Errorf ( "lock.Lock: %w" , err )
}
defer releaser ( )
2022-05-04 18:06:23 +02:00
2024-09-06 18:12:41 +08:00
err = db . WithTx ( ctx , func ( ctx context . Context ) error {
2023-01-16 16:00:22 +08:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
return err
}
2022-12-10 10:46:31 +08:00
prUnit , err := pr . BaseRepo . GetUnit ( ctx , unit . TypePullRequests )
2022-05-03 21:46:28 +02:00
if err != nil {
return err
}
prConfig := prUnit . PullRequestsConfig ( )
2021-03-04 11:41:23 +08:00
2022-05-03 21:46:28 +02:00
// Check if merge style is correct and allowed
if ! prConfig . IsMergeStyleAllowed ( repo_model . MergeStyleManuallyMerged ) {
2024-12-20 10:05:29 -08:00
return ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : repo_model . MergeStyleManuallyMerged }
2022-05-03 21:46:28 +02:00
}
2021-03-04 11:41:23 +08:00
2024-02-24 14:55:19 +08:00
objectFormat := git . ObjectFormatFromName ( pr . BaseRepo . ObjectFormatName )
2023-12-13 21:02:00 +00:00
if len ( commitID ) != objectFormat . FullLength ( ) {
2025-04-01 12:14:01 +02:00
return errors . New ( "Wrong commit ID" )
2021-03-04 11:41:23 +08:00
}
2022-05-03 21:46:28 +02:00
commit , err := baseGitRepo . GetCommit ( commitID )
if err != nil {
if git . IsErrNotExist ( err ) {
2025-04-01 12:14:01 +02:00
return errors . New ( "Wrong commit ID" )
2022-05-03 21:46:28 +02:00
}
return err
}
commitID = commit . ID . String ( )
2021-03-04 11:41:23 +08:00
2022-05-03 21:46:28 +02:00
ok , err := baseGitRepo . IsCommitInBranch ( commitID , pr . BaseBranch )
if err != nil {
return err
}
if ! ok {
2025-04-01 12:14:01 +02:00
return errors . New ( "Wrong commit ID" )
2022-05-03 21:46:28 +02:00
}
2021-03-04 11:41:23 +08:00
2022-06-20 12:02:49 +02:00
var merged bool
2025-01-07 19:16:56 -08:00
if merged , err = SetMerged ( ctx , pr , commitID , timeutil . TimeStamp ( commit . Author . When . Unix ( ) ) , doer , issues_model . PullRequestStatusManuallyMerged ) ; err != nil {
2022-05-03 21:46:28 +02:00
return err
} else if ! merged {
2025-04-01 12:14:01 +02:00
return errors . New ( "SetMerged failed" )
2022-05-03 21:46:28 +02:00
}
return nil
2024-09-06 18:12:41 +08:00
} )
releaser ( )
if err != nil {
2022-05-03 21:46:28 +02:00
return err
2021-03-04 11:41:23 +08:00
}
2025-10-31 21:56:08 -07:00
notify_service . MergePullRequest ( ctx , doer , pr )
2022-05-03 21:46:28 +02:00
log . Info ( "manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s" , pr . ID , pr . BaseRepo . Name , pr . BaseBranch , commitID )
2024-08-26 01:18:19 +08:00
return handleCloseCrossReferences ( ctx , pr , doer )
2021-03-04 11:41:23 +08:00
}
2024-12-29 23:04:03 -08:00
// SetMerged sets a pull request to merged and closes the corresponding issue
2025-01-07 19:16:56 -08:00
func SetMerged ( ctx context . Context , pr * issues_model . PullRequest , mergedCommitID string , mergedTimeStamp timeutil . TimeStamp , merger * user_model . User , mergeStatus issues_model . PullRequestStatus ) ( bool , error ) {
2024-12-29 23:04:03 -08:00
if pr . HasMerged {
return false , fmt . Errorf ( "PullRequest[%d] already merged" , pr . Index )
}
pr . HasMerged = true
2025-01-07 19:16:56 -08:00
pr . MergedCommitID = mergedCommitID
pr . MergedUnix = mergedTimeStamp
pr . Merger = merger
pr . MergerID = merger . ID
pr . Status = mergeStatus
// reset the conflicted files as there cannot be any if we're merged
pr . ConflictedFiles = [ ] string { }
2024-12-29 23:04:03 -08:00
2025-01-07 19:16:56 -08:00
if pr . MergedCommitID == "" || pr . MergedUnix == 0 || pr . Merger == nil {
return false , fmt . Errorf ( "unable to merge PullRequest[%d], some required fields are empty" , pr . Index )
2024-12-29 23:04:03 -08:00
}
2025-07-23 01:02:01 +08:00
return db . WithTx2 ( ctx , func ( ctx context . Context ) ( bool , error ) {
pr . Issue = nil
if err := pr . LoadIssue ( ctx ) ; err != nil {
return false , err
}
2024-12-29 23:04:03 -08:00
2025-07-23 01:02:01 +08:00
if err := pr . Issue . LoadRepo ( ctx ) ; err != nil {
return false , err
}
2024-12-29 23:04:03 -08:00
2025-07-23 01:02:01 +08:00
if err := pr . Issue . Repo . LoadOwner ( ctx ) ; err != nil {
return false , err
}
2024-12-29 23:04:03 -08:00
2025-07-23 01:02:01 +08:00
// Removing an auto merge pull and ignore if not exist
if err := pull_model . DeleteScheduledAutoMerge ( ctx , pr . ID ) ; err != nil && ! db . IsErrNotExist ( err ) {
return false , fmt . Errorf ( "DeleteScheduledAutoMerge[%d]: %v" , pr . ID , err )
}
2024-12-29 23:04:03 -08:00
2025-07-23 01:02:01 +08:00
// Set issue as closed
if _ , err := issues_model . SetIssueAsClosed ( ctx , pr . Issue , pr . Merger , true ) ; err != nil {
return false , fmt . Errorf ( "ChangeIssueStatus: %w" , err )
}
2025-01-07 19:16:56 -08:00
2026-01-27 11:57:20 -08:00
// We need to save all of the data used to compute this merge as it may have already been changed by checkPullRequestBranchMergeable. FIXME: need to set some state to prevent checkPullRequestBranchMergeable from running whilst we are merging.
2025-07-23 01:02:01 +08:00
if cnt , err := db . GetEngine ( ctx ) . Where ( "id = ?" , pr . ID ) .
And ( "has_merged = ?" , false ) .
Cols ( "has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files" ) .
Update ( pr ) ; err != nil {
return false , fmt . Errorf ( "failed to update pr[%d]: %w" , pr . ID , err )
} else if cnt != 1 {
return false , issues_model . ErrIssueAlreadyChanged
}
2024-12-29 23:04:03 -08:00
2025-07-23 01:02:01 +08:00
return true , nil
} )
2024-12-29 23:04:03 -08:00
}
2025-10-21 22:06:56 -07:00
func ShouldDeleteBranchAfterMerge ( ctx context . Context , userOption * bool , repo * repo_model . Repository , pr * issues_model . PullRequest ) ( bool , error ) {
if pr . Flow != issues_model . PullRequestFlowGithub {
// only support GitHub workflow (branch-based)
// for agit workflow, there is no branch, so nothing to delete
// TODO: maybe in the future, it should delete the "agit reference (refs/for/xxxx)"?
return false , nil
}
// if user has set an option, respect it
if userOption != nil {
return * userOption , nil
}
// otherwise, use repository default
prUnit , err := repo . GetUnit ( ctx , unit . TypePullRequests )
if err != nil {
return false , err
}
return prUnit . PullRequestsConfig ( ) . DefaultDeleteBranchAfterMerge , nil
}