2019-06-23 01:35:34 +08:00
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pull
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
2019-11-01 08:30:02 +08:00
"regexp"
2019-06-23 01:35:34 +08:00
"strings"
2019-10-16 21:42:42 +08:00
"time"
2019-06-23 01:35:34 +08:00
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
2019-11-05 19:04:08 +08:00
"code.gitea.io/gitea/modules/notification"
2019-11-18 21:13:07 +08:00
"code.gitea.io/gitea/modules/references"
2019-06-23 01:35:34 +08:00
"code.gitea.io/gitea/modules/setting"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2019-11-18 21:13:07 +08:00
issue_service "code.gitea.io/gitea/services/issue"
2019-06-23 01:35:34 +08:00
)
// Merge merges pull request to base repository.
2020-01-11 15:29:34 +08:00
// Caller should check PR is ready to be merged (review and status checks)
2019-06-23 01:35:34 +08:00
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
func Merge ( pr * models . PullRequest , doer * models . User , baseGitRepo * git . Repository , mergeStyle models . MergeStyle , message string ) ( err error ) {
2020-03-03 06:31:55 +08:00
if err = pr . LoadHeadRepo ( ) ; err != nil {
log . Error ( "LoadHeadRepo: %v" , err )
return fmt . Errorf ( "LoadHeadRepo: %v" , err )
} else if err = pr . LoadBaseRepo ( ) ; err != nil {
log . Error ( "LoadBaseRepo: %v" , err )
return fmt . Errorf ( "LoadBaseRepo: %v" , err )
2019-06-23 01:35:34 +08:00
}
prUnit , err := pr . BaseRepo . GetUnit ( models . UnitTypePullRequests )
if err != nil {
2019-11-10 16:42:51 +08:00
log . Error ( "pr.BaseRepo.GetUnit(models.UnitTypePullRequests): %v" , err )
2019-06-23 01:35:34 +08:00
return err
}
prConfig := prUnit . PullRequestsConfig ( )
// Check if merge style is correct and allowed
if ! prConfig . IsMergeStyleAllowed ( mergeStyle ) {
return models . ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : mergeStyle }
}
defer func ( ) {
2020-01-09 09:47:45 +08:00
go AddTestPullRequestTask ( doer , pr . BaseRepo . ID , pr . BaseBranch , false , "" , "" )
2019-06-23 01:35:34 +08:00
} ( )
2020-02-10 07:09:31 +08:00
pr . MergedCommitID , err = rawMerge ( pr , doer , mergeStyle , message )
2020-01-17 14:03:40 +08:00
if err != nil {
2020-02-10 07:09:31 +08:00
return err
2020-01-17 14:03:40 +08:00
}
pr . MergedUnix = timeutil . TimeStampNow ( )
pr . Merger = doer
pr . MergerID = doer . ID
2021-04-10 20:08:30 +08:00
if _ , err := pr . SetMerged ( ) ; err != nil {
2020-01-17 14:03:40 +08:00
log . Error ( "setMerged [%d]: %v" , pr . ID , err )
}
2020-02-10 07:09:31 +08:00
if err := pr . LoadIssue ( ) ; err != nil {
log . Error ( "loadIssue [%d]: %v" , pr . ID , err )
}
if err := pr . Issue . LoadRepo ( ) ; err != nil {
log . Error ( "loadRepo for issue [%d]: %v" , pr . ID , err )
}
if err := pr . Issue . Repo . GetOwner ( ) ; err != nil {
log . Error ( "GetOwner for issue repo [%d]: %v" , pr . ID , err )
}
2020-01-17 14:03:40 +08:00
notification . NotifyMergePullRequest ( pr , doer )
// Reset cached commit count
cache . Remove ( pr . Issue . Repo . GetCommitsCountCacheKey ( pr . BaseBranch , true ) )
// Resolve cross references
refs , err := pr . ResolveCrossReferences ( )
if err != nil {
log . Error ( "ResolveCrossReferences: %v" , err )
return nil
}
for _ , ref := range refs {
if err = ref . LoadIssue ( ) ; err != nil {
return err
}
if err = ref . Issue . LoadRepo ( ) ; err != nil {
return err
}
close := ( ref . RefAction == references . XRefActionCloses )
if close != ref . Issue . IsClosed {
if err = issue_service . ChangeStatus ( ref . Issue , doer , close ) ; err != nil {
return err
}
}
}
return nil
}
// rawMerge perform the merge operation without changing any pull information in database
2020-02-10 07:09:31 +08:00
func rawMerge ( pr * models . PullRequest , doer * models . User , mergeStyle models . MergeStyle , message string ) ( string , error ) {
2020-09-06 00:42:58 +08:00
err := git . LoadGitVersion ( )
2020-01-17 14:03:40 +08:00
if err != nil {
2020-09-06 00:42:58 +08:00
log . Error ( "git.LoadGitVersion: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Unable to get git version: %v" , err )
2020-01-17 14:03:40 +08:00
}
2019-06-23 01:35:34 +08:00
// Clone base repo.
2019-12-14 06:21:06 +08:00
tmpBasePath , err := createTemporaryRepo ( pr )
2019-06-23 01:35:34 +08:00
if err != nil {
2019-11-10 16:42:51 +08:00
log . Error ( "CreateTemporaryPath: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-06-23 01:35:34 +08:00
}
defer func ( ) {
if err := models . RemoveTemporaryPath ( tmpBasePath ) ; err != nil {
log . Error ( "Merge: RemoveTemporaryPath: %s" , err )
}
} ( )
2019-10-12 08:13:27 +08:00
baseBranch := "base"
trackingBranch := "tracking"
stagingBranch := "staging"
2019-06-23 01:35:34 +08:00
2019-12-14 06:21:06 +08:00
var outbuf , errbuf strings . Builder
2019-06-23 01:35:34 +08:00
// Enable sparse-checkout
2019-10-12 08:13:27 +08:00
sparseCheckoutList , err := getDiffTree ( tmpBasePath , baseBranch , trackingBranch )
2019-06-23 01:35:34 +08:00
if err != nil {
2019-11-10 16:42:51 +08:00
log . Error ( "getDiffTree(%s, %s, %s): %v" , tmpBasePath , baseBranch , trackingBranch , err )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "getDiffTree: %v" , err )
2019-06-23 01:35:34 +08:00
}
infoPath := filepath . Join ( tmpBasePath , ".git" , "info" )
if err := os . MkdirAll ( infoPath , 0700 ) ; err != nil {
2019-11-10 16:42:51 +08:00
log . Error ( "Unable to create .git/info in %s: %v" , tmpBasePath , err )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Unable to create .git/info in tmpBasePath: %v" , err )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
2019-06-23 01:35:34 +08:00
sparseCheckoutListPath := filepath . Join ( infoPath , "sparse-checkout" )
if err := ioutil . WriteFile ( sparseCheckoutListPath , [ ] byte ( sparseCheckoutList ) , 0600 ) ; err != nil {
2019-11-10 16:42:51 +08:00
log . Error ( "Unable to write .git/info/sparse-checkout file in %s: %v" , tmpBasePath , err )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Unable to write .git/info/sparse-checkout file in tmpBasePath: %v" , err )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
var gitConfigCommand func ( ) * git . Command
2020-10-21 23:42:08 +08:00
if git . CheckGitVersionAtLeast ( "1.8.0" ) == nil {
2019-11-10 16:42:51 +08:00
gitConfigCommand = func ( ) * git . Command {
return git . NewCommand ( "config" , "--local" )
2019-10-12 08:13:27 +08:00
}
2019-11-10 16:42:51 +08:00
} else {
gitConfigCommand = func ( ) * git . Command {
2019-10-12 08:13:27 +08:00
return git . NewCommand ( "config" )
}
2019-11-10 16:42:51 +08:00
}
2019-10-12 08:13:27 +08:00
2019-06-23 01:35:34 +08:00
// Switch off LFS process (set required, clean and smudge here also)
2019-11-10 16:42:51 +08:00
if err := gitConfigCommand ( ) . AddArguments ( "filter.lfs.process" , "" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git config [filter.lfs.process -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git config [filter.lfs.process -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
if err := gitConfigCommand ( ) . AddArguments ( "filter.lfs.required" , "false" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git config [filter.lfs.required -> <false> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git config [filter.lfs.required -> <false> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
if err := gitConfigCommand ( ) . AddArguments ( "filter.lfs.clean" , "" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git config [filter.lfs.clean -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git config [filter.lfs.clean -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
if err := gitConfigCommand ( ) . AddArguments ( "filter.lfs.smudge" , "" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
2019-11-10 16:42:51 +08:00
if err := gitConfigCommand ( ) . AddArguments ( "core.sparseCheckout" , "true" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git config [core.sparseCheckout -> true ]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git config [core.sparsecheckout -> true]: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
// Read base branch index
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "read-tree" , "HEAD" ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git read-tree HEAD: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Unable to read base branch in to the index: %v\n%s\n%s" , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
2020-09-20 00:44:55 +08:00
sig := doer . NewGitSig ( )
committer := sig
2019-10-16 21:42:42 +08:00
// Determine if we should sign
signArg := ""
2020-10-21 23:42:08 +08:00
if git . CheckGitVersionAtLeast ( "1.7.9" ) == nil {
2020-09-20 00:44:55 +08:00
sign , keyID , signer , _ := pr . SignMerge ( doer , tmpBasePath , "HEAD" , trackingBranch )
2019-10-16 21:42:42 +08:00
if sign {
signArg = "-S" + keyID
2020-09-20 00:44:55 +08:00
if pr . BaseRepo . GetTrustModel ( ) == models . CommitterTrustModel || pr . BaseRepo . GetTrustModel ( ) == models . CollaboratorCommitterTrustModel {
committer = signer
}
2020-10-21 23:42:08 +08:00
} else if git . CheckGitVersionAtLeast ( "2.0.0" ) == nil {
2019-10-16 21:42:42 +08:00
signArg = "--no-gpg-sign"
}
}
commitTimeStr := time . Now ( ) . Format ( time . RFC3339 )
// Because this may call hooks we should pass in the environment
env := append ( os . Environ ( ) ,
"GIT_AUTHOR_NAME=" + sig . Name ,
"GIT_AUTHOR_EMAIL=" + sig . Email ,
"GIT_AUTHOR_DATE=" + commitTimeStr ,
2020-09-20 00:44:55 +08:00
"GIT_COMMITTER_NAME=" + committer . Name ,
"GIT_COMMITTER_EMAIL=" + committer . Email ,
2019-10-16 21:42:42 +08:00
"GIT_COMMITTER_DATE=" + commitTimeStr ,
)
2019-06-23 01:35:34 +08:00
// Merge commits.
switch mergeStyle {
case models . MergeStyleMerge :
2019-11-10 16:42:51 +08:00
cmd := git . NewCommand ( "merge" , "--no-ff" , "--no-commit" , trackingBranch )
if err := runMergeCommand ( pr , mergeStyle , cmd , tmpBasePath ) ; err != nil {
log . Error ( "Unable to merge tracking into base: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
if err := commitAndSignNoAuthor ( pr , message , signArg , tmpBasePath , env ) ; err != nil {
log . Error ( "Unable to make final commit: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-06-23 01:35:34 +08:00
}
case models . MergeStyleRebase :
2019-11-10 16:42:51 +08:00
fallthrough
2019-06-23 01:35:34 +08:00
case models . MergeStyleRebaseMerge :
// Checkout head branch
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "checkout" , "-b" , stagingBranch , trackingBranch ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
// Rebase before merging
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "rebase" , baseBranch ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
// Rebase will leave a REBASE_HEAD file in .git if there is a conflict
if _ , statErr := os . Stat ( filepath . Join ( tmpBasePath , ".git" , "REBASE_HEAD" ) ) ; statErr == nil {
2020-04-04 00:00:41 +08:00
var commitSha string
ok := false
failingCommitPaths := [ ] string {
filepath . Join ( tmpBasePath , ".git" , "rebase-apply" , "original-commit" ) , // Git < 2.26
filepath . Join ( tmpBasePath , ".git" , "rebase-merge" , "stopped-sha" ) , // Git >= 2.26
}
for _ , failingCommitPath := range failingCommitPaths {
if _ , statErr := os . Stat ( filepath . Join ( failingCommitPath ) ) ; statErr == nil {
commitShaBytes , readErr := ioutil . ReadFile ( filepath . Join ( failingCommitPath ) )
if readErr != nil {
// Abandon this attempt to handle the error
log . Error ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return "" , fmt . Errorf ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
}
commitSha = strings . TrimSpace ( string ( commitShaBytes ) )
ok = true
break
}
}
if ! ok {
log . Error ( "Unable to determine failing commit sha for this rebase message. Cannot cast as models.ErrRebaseConflicts." )
2019-11-10 16:42:51 +08:00
log . Error ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-11-10 16:42:51 +08:00
}
2020-04-04 00:00:41 +08:00
log . Debug ( "RebaseConflict at %s [%s:%s -> %s:%s]: %v\n%s\n%s" , commitSha , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , models . ErrRebaseConflicts {
2019-11-10 16:42:51 +08:00
Style : mergeStyle ,
2020-04-04 00:00:41 +08:00
CommitSHA : commitSha ,
2019-11-10 16:42:51 +08:00
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
}
log . Error ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
// Checkout base branch again
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "checkout" , baseBranch ) . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
2019-11-10 16:42:51 +08:00
cmd := git . NewCommand ( "merge" )
if mergeStyle == models . MergeStyleRebase {
cmd . AddArguments ( "--ff-only" )
2019-10-16 21:42:42 +08:00
} else {
2019-11-10 16:42:51 +08:00
cmd . AddArguments ( "--no-ff" , "--no-commit" )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
cmd . AddArguments ( stagingBranch )
2019-06-23 01:35:34 +08:00
2019-11-10 16:42:51 +08:00
// Prepare merge with commit
if err := runMergeCommand ( pr , mergeStyle , cmd , tmpBasePath ) ; err != nil {
log . Error ( "Unable to merge staging into base: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-11-10 16:42:51 +08:00
}
if mergeStyle == models . MergeStyleRebaseMerge {
if err := commitAndSignNoAuthor ( pr , message , signArg , tmpBasePath , env ) ; err != nil {
log . Error ( "Unable to make final commit: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-11-10 16:42:51 +08:00
}
}
2019-06-23 01:35:34 +08:00
case models . MergeStyleSquash :
// Merge with squash
2019-11-10 16:42:51 +08:00
cmd := git . NewCommand ( "merge" , "--squash" , trackingBranch )
if err := runMergeCommand ( pr , mergeStyle , cmd , tmpBasePath ) ; err != nil {
log . Error ( "Unable to merge --squash tracking into base: %v" , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
2020-04-10 18:40:36 +08:00
if err = pr . Issue . LoadPoster ( ) ; err != nil {
log . Error ( "LoadPoster: %v" , err )
return "" , fmt . Errorf ( "LoadPoster: %v" , err )
}
2019-06-23 01:35:34 +08:00
sig := pr . Issue . Poster . NewGitSig ( )
2019-10-16 21:42:42 +08:00
if signArg == "" {
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "commit" , fmt . Sprintf ( "--author='%s <%s>'" , sig . Name , sig . Email ) , "-m" , message ) . RunInDirTimeoutEnvPipeline ( env , - 1 , tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-10-16 21:42:42 +08:00
}
} else {
2020-09-20 00:44:55 +08:00
if committer != sig {
// add trailer
2020-12-22 10:19:33 +08:00
message += fmt . Sprintf ( "\nCo-authored-by: %s\nCo-committed-by: %s\n" , sig . String ( ) , sig . String ( ) )
2020-09-20 00:44:55 +08:00
}
2019-11-10 16:42:51 +08:00
if err := git . NewCommand ( "commit" , signArg , fmt . Sprintf ( "--author='%s <%s>'" , sig . Name , sig . Email ) , "-m" , message ) . RunInDirTimeoutEnvPipeline ( env , - 1 , tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
2019-10-16 21:42:42 +08:00
}
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
default :
2020-02-10 07:09:31 +08:00
return "" , models . ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : mergeStyle }
2019-06-23 01:35:34 +08:00
}
// OK we should cache our current head and origin/headbranch
mergeHeadSHA , err := git . GetFullCommitID ( tmpBasePath , "HEAD" )
if err != nil {
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Failed to get full commit id for HEAD: %v" , err )
2019-06-23 01:35:34 +08:00
}
2019-10-12 08:13:27 +08:00
mergeBaseSHA , err := git . GetFullCommitID ( tmpBasePath , "original_" + baseBranch )
2019-06-23 01:35:34 +08:00
if err != nil {
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "Failed to get full commit id for origin/%s: %v" , pr . BaseBranch , err )
}
mergeCommitID , err := git . GetFullCommitID ( tmpBasePath , baseBranch )
if err != nil {
return "" , fmt . Errorf ( "Failed to get full commit id for the new merge: %v" , err )
2019-06-23 01:35:34 +08: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 {
if err := LFSPush ( tmpBasePath , mergeHeadSHA , mergeBaseSHA , pr ) ; err != nil {
2020-02-10 07:09:31 +08:00
return "" , err
2019-06-23 01:35:34 +08:00
}
}
2019-10-18 19:13:31 +08:00
var headUser * models . User
err = pr . HeadRepo . GetOwner ( )
2019-07-01 09:18:13 +08:00
if err != nil {
if ! models . IsErrUserNotExist ( err ) {
2019-10-18 19:13:31 +08:00
log . Error ( "Can't find user: %d for head repository - %v" , pr . HeadRepo . OwnerID , err )
2020-02-10 07:09:31 +08:00
return "" , err
2019-07-01 09:18:13 +08:00
}
2019-10-18 19:13:31 +08:00
log . Error ( "Can't find user: %d for head repository - defaulting to doer: %s - %v" , pr . HeadRepo . OwnerID , doer . Name , err )
2019-07-01 09:18:13 +08:00
headUser = doer
2019-10-18 19:13:31 +08:00
} else {
headUser = pr . HeadRepo . Owner
2019-07-01 09:18:13 +08:00
}
2019-10-16 21:42:42 +08:00
env = models . FullPushingEnvironment (
2019-07-26 05:50:20 +08:00
headUser ,
doer ,
pr . BaseRepo ,
pr . BaseRepo . Name ,
pr . ID ,
)
2019-06-23 01:35:34 +08:00
// Push back to upstream.
2020-12-08 09:46:50 +08:00
if err := git . NewCommand ( "push" , "origin" , baseBranch + ":refs/heads/" + pr . BaseBranch ) . RunInDirTimeoutEnvPipeline ( env , - 1 , tmpBasePath , & outbuf , & errbuf ) ; err != nil {
2019-11-10 16:42:51 +08:00
if strings . Contains ( errbuf . String ( ) , "non-fast-forward" ) {
2020-03-28 12:13:18 +08:00
return "" , & git . ErrPushOutOfDate {
2019-11-10 16:42:51 +08:00
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
2020-02-22 21:08:48 +08:00
} else if strings . Contains ( errbuf . String ( ) , "! [remote rejected]" ) {
2020-03-28 12:13:18 +08:00
err := & git . ErrPushRejected {
2020-02-22 21:08:48 +08:00
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
err . GenerateMessage ( )
return "" , err
2019-11-10 16:42:51 +08:00
}
2020-02-10 07:09:31 +08:00
return "" , fmt . Errorf ( "git push: %s" , errbuf . String ( ) )
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
outbuf . Reset ( )
errbuf . Reset ( )
2019-06-23 01:35:34 +08:00
2020-02-10 07:09:31 +08:00
return mergeCommitID , nil
2019-06-23 01:35:34 +08:00
}
2019-11-10 16:42:51 +08:00
func commitAndSignNoAuthor ( pr * models . PullRequest , message , signArg , tmpBasePath string , env [ ] string ) error {
var outbuf , errbuf strings . Builder
if signArg == "" {
if err := git . NewCommand ( "commit" , "-m" , message ) . RunInDirTimeoutEnvPipeline ( env , - 1 , tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return fmt . Errorf ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
}
} else {
if err := git . NewCommand ( "commit" , signArg , "-m" , message ) . RunInDirTimeoutEnvPipeline ( env , - 1 , tmpBasePath , & outbuf , & errbuf ) ; err != nil {
log . Error ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return fmt . Errorf ( "git commit [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
}
}
return nil
}
func runMergeCommand ( pr * models . PullRequest , mergeStyle models . MergeStyle , cmd * git . Command , tmpBasePath string ) error {
var outbuf , errbuf strings . Builder
if err := cmd . RunInDirPipeline ( tmpBasePath , & outbuf , & errbuf ) ; err != nil {
// Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict
if _ , statErr := os . Stat ( filepath . Join ( tmpBasePath , ".git" , "MERGE_HEAD" ) ) ; statErr == nil {
// We have a merge conflict error
log . Debug ( "MergeConflict [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return models . ErrMergeConflicts {
Style : mergeStyle ,
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
} else if strings . Contains ( errbuf . String ( ) , "refusing to merge unrelated histories" ) {
log . Debug ( "MergeUnrelatedHistories [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return models . ErrMergeUnrelatedHistories {
Style : mergeStyle ,
StdOut : outbuf . String ( ) ,
StdErr : errbuf . String ( ) ,
Err : err ,
}
}
log . Error ( "git merge [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
return fmt . Errorf ( "git merge [%s:%s -> %s:%s]: %v\n%s\n%s" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , pr . BaseBranch , err , outbuf . String ( ) , errbuf . String ( ) )
}
return nil
}
2019-11-01 08:30:02 +08:00
var escapedSymbols = regexp . MustCompile ( ` ([*[?! \\]) ` )
2019-06-23 01:35:34 +08:00
func getDiffTree ( repoPath , baseBranch , headBranch string ) ( string , error ) {
getDiffTreeFromBranch := func ( repoPath , baseBranch , headBranch string ) ( string , error ) {
var outbuf , errbuf strings . Builder
// Compute the diff-tree for sparse-checkout
2019-11-01 08:30:02 +08:00
if err := git . NewCommand ( "diff-tree" , "--no-commit-id" , "--name-only" , "-r" , "-z" , "--root" , baseBranch , headBranch , "--" ) . RunInDirPipeline ( repoPath , & outbuf , & errbuf ) ; err != nil {
2019-06-23 01:35:34 +08:00
return "" , fmt . Errorf ( "git diff-tree [%s base:%s head:%s]: %s" , repoPath , baseBranch , headBranch , errbuf . String ( ) )
}
return outbuf . String ( ) , nil
}
2019-11-01 08:30:02 +08:00
scanNullTerminatedStrings := func ( data [ ] byte , atEOF bool ) ( advance int , token [ ] byte , err error ) {
if atEOF && len ( data ) == 0 {
return 0 , nil , nil
}
if i := bytes . IndexByte ( data , '\x00' ) ; i >= 0 {
return i + 1 , data [ 0 : i ] , nil
}
if atEOF {
return len ( data ) , data , nil
}
return 0 , nil , nil
}
2019-06-23 01:35:34 +08:00
list , err := getDiffTreeFromBranch ( repoPath , baseBranch , headBranch )
if err != nil {
return "" , err
}
// Prefixing '/' for each entry, otherwise all files with the same name in subdirectories would be matched.
out := bytes . Buffer { }
scanner := bufio . NewScanner ( strings . NewReader ( list ) )
2019-11-01 08:30:02 +08:00
scanner . Split ( scanNullTerminatedStrings )
2019-06-23 01:35:34 +08:00
for scanner . Scan ( ) {
2019-11-01 08:30:02 +08:00
filepath := scanner . Text ( )
// escape '*', '?', '[', spaces and '!' prefix
filepath = escapedSymbols . ReplaceAllString ( filepath , ` \$1 ` )
// no necessary to escape the first '#' symbol because the first symbol is '/'
fmt . Fprintf ( & out , "/%s\n" , filepath )
2019-06-23 01:35:34 +08:00
}
2019-11-01 08:30:02 +08:00
2019-06-23 01:35:34 +08:00
return out . String ( ) , nil
}
2020-01-11 15:29:34 +08:00
2020-01-15 16:32:57 +08:00
// IsSignedIfRequired check if merge will be signed if required
func IsSignedIfRequired ( pr * models . PullRequest , doer * models . User ) ( bool , error ) {
if err := pr . LoadProtectedBranch ( ) ; err != nil {
return false , err
}
if pr . ProtectedBranch == nil || ! pr . ProtectedBranch . RequireSignedCommits {
return true , nil
}
2020-09-20 00:44:55 +08:00
sign , _ , _ , err := pr . SignMerge ( doer , pr . BaseRepo . RepoPath ( ) , pr . BaseBranch , pr . GetGitRefName ( ) )
2020-01-15 16:32:57 +08:00
return sign , err
}
2020-01-11 15:29:34 +08:00
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
func IsUserAllowedToMerge ( pr * models . PullRequest , p models . Permission , user * models . User ) ( bool , error ) {
2020-04-15 00:29:31 +08:00
if user == nil {
return false , nil
}
2020-01-11 15:29:34 +08:00
err := pr . LoadProtectedBranch ( )
if err != nil {
return false , err
}
2020-08-20 15:48:40 +08:00
if ( p . CanWrite ( models . UnitTypeCode ) && pr . ProtectedBranch == nil ) || ( pr . ProtectedBranch != nil && pr . ProtectedBranch . IsUserMergeWhitelisted ( user . ID , p ) ) {
2020-01-11 15:29:34 +08:00
return true , nil
}
return false , nil
}
// CheckPRReadyToMerge checks whether the PR is ready to be merged (reviews and status checks)
2020-10-14 02:50:57 +08:00
func CheckPRReadyToMerge ( pr * models . PullRequest , skipProtectedFilesCheck bool ) ( err error ) {
2020-03-03 06:31:55 +08:00
if err = pr . LoadBaseRepo ( ) ; err != nil {
return fmt . Errorf ( "LoadBaseRepo: %v" , err )
}
if err = pr . LoadProtectedBranch ( ) ; err != nil {
return fmt . Errorf ( "LoadProtectedBranch: %v" , err )
2020-01-11 15:29:34 +08:00
}
if pr . ProtectedBranch == nil {
2020-03-03 06:31:55 +08:00
return nil
2020-01-11 15:29:34 +08:00
}
isPass , err := IsPullCommitStatusPass ( pr )
if err != nil {
return err
}
if ! isPass {
return models . ErrNotAllowedToMerge {
Reason : "Not all required status checks successful" ,
}
}
2020-04-17 09:00:36 +08:00
if ! pr . ProtectedBranch . HasEnoughApprovals ( pr ) {
2020-01-11 15:29:34 +08:00
return models . ErrNotAllowedToMerge {
Reason : "Does not have enough approvals" ,
}
}
2020-04-17 09:00:36 +08:00
if pr . ProtectedBranch . MergeBlockedByRejectedReview ( pr ) {
2020-01-11 15:29:34 +08:00
return models . ErrNotAllowedToMerge {
Reason : "There are requested changes" ,
}
}
2020-11-29 03:30:46 +08:00
if pr . ProtectedBranch . MergeBlockedByOfficialReviewRequests ( pr ) {
return models . ErrNotAllowedToMerge {
Reason : "There are official review requests" ,
}
}
2020-01-11 15:29:34 +08:00
2020-04-17 09:00:36 +08:00
if pr . ProtectedBranch . MergeBlockedByOutdatedBranch ( pr ) {
return models . ErrNotAllowedToMerge {
Reason : "The head branch is behind the base branch" ,
}
}
2020-10-14 02:50:57 +08:00
if skipProtectedFilesCheck {
return nil
}
if pr . ProtectedBranch . MergeBlockedByProtectedFiles ( pr ) {
return models . ErrNotAllowedToMerge {
Reason : "Changed protected files" ,
}
}
2020-01-11 15:29:34 +08:00
return nil
}
2021-03-04 11:41:23 +08:00
// MergedManually mark pr as merged manually
func MergedManually ( pr * models . PullRequest , doer * models . User , baseGitRepo * git . Repository , commitID string ) ( err error ) {
prUnit , err := pr . BaseRepo . GetUnit ( models . UnitTypePullRequests )
if err != nil {
return
}
prConfig := prUnit . PullRequestsConfig ( )
// Check if merge style is correct and allowed
if ! prConfig . IsMergeStyleAllowed ( models . MergeStyleManuallyMerged ) {
return models . ErrInvalidMergeStyle { ID : pr . BaseRepo . ID , Style : models . MergeStyleManuallyMerged }
}
if len ( commitID ) < 40 {
return fmt . Errorf ( "Wrong commit ID" )
}
commit , err := baseGitRepo . GetCommit ( commitID )
if err != nil {
if git . IsErrNotExist ( err ) {
return fmt . Errorf ( "Wrong commit ID" )
}
return
}
ok , err := baseGitRepo . IsCommitInBranch ( commitID , pr . BaseBranch )
if err != nil {
return
}
if ! ok {
return fmt . Errorf ( "Wrong commit ID" )
}
pr . MergedCommitID = commitID
pr . MergedUnix = timeutil . TimeStamp ( commit . Author . When . Unix ( ) )
pr . Status = models . PullRequestStatusManuallyMerged
pr . Merger = doer
pr . MergerID = doer . ID
merged := false
if merged , err = pr . SetMerged ( ) ; err != nil {
return
} else if ! merged {
return fmt . Errorf ( "SetMerged failed" )
}
notification . NotifyMergePullRequest ( pr , doer )
log . Info ( "manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s" , pr . ID , pr . BaseRepo . Name , pr . BaseBranch , commit . ID . String ( ) )
return nil
}