@ -6,7 +6,6 @@ package models
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
@ -18,6 +17,7 @@ import (
"github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process"
"github.com/gogits/gogs/modules/setting"
)
type PullRequestType int
@ -48,13 +48,14 @@ type PullRequest struct {
HeadRepoID int64
HeadRepo * Repository ` xorm:"-" `
BaseRepoID int64
BaseRepo * Repository ` xorm:"-" `
HeadUserName string
HeadBranch string
BaseBranch string
MergeBase string ` xorm:"VARCHAR(40)" `
MergedCommitID string ` xorm:"VARCHAR(40)" `
HasMerged bool
MergedCommitID string ` xorm:"VARCHAR(40)" `
Merged time . Time
MergerID int64
Merger * User ` xorm:"-" `
@ -62,35 +63,54 @@ type PullRequest struct {
// Note: don't try to get Pull because will end up recursive querying.
func ( pr * PullRequest ) AfterSet ( colName string , _ xorm . Cell ) {
var err error
switch colName {
case "head_repo_id" :
// FIXME: shouldn't show error if it's known that head repository has been removed.
case "merged" :
if ! pr . HasMerged {
return
}
pr . Merged = regulateTimeZone ( pr . Merged )
}
}
func ( pr * PullRequest ) GetHeadRepo ( ) ( err error ) {
pr . HeadRepo , err = GetRepositoryByID ( pr . HeadRepoID )
if err != nil && ! IsErrRepoNotExist ( err ) {
return fmt . Errorf ( "GetRepositoryByID (head): %v" , err )
}
return nil
}
func ( pr * PullRequest ) GetBaseRepo ( ) ( err error ) {
if pr . BaseRepo != nil {
return nil
}
pr . BaseRepo , err = GetRepositoryByID ( pr . BaseRepoID )
if err != nil {
log . Error ( 3 , "GetRepositoryByID[%d]: %v" , pr . ID , err )
return fmt . Errorf ( "GetRepositoryByID (base): %v" , err )
}
case "merger_id" :
if ! pr . HasMerged {
return
return nil
}
func ( pr * PullRequest ) GetMerger ( ) ( err error ) {
if ! pr . HasMerged || pr . Merger != nil {
return nil
}
pr . Merger , err = GetUserByID ( pr . MergerID )
if err != nil {
if IsErrUserNotExist ( err ) {
pr . MergerID = - 1
pr . Merger = NewFakeUser ( )
} else {
log . Error ( 3 , "GetUserByID[%d]: %v" , pr . ID , err )
} else if err != nil {
return fmt . Errorf ( "GetUserByID: %v" , err )
}
}
case "merged" :
if ! pr . HasMerged {
return
return nil
}
pr . Merged = regulateTimeZone ( pr . Merged )
}
// IsChecking returns true if this pull request is still checking conflict.
func ( pr * PullRequest ) IsChecking ( ) bool {
return pr . Status == PULL_REQUEST_STATUS_CHECKING
}
// CanAutoMerge returns true if this pull request can be merged automatically.
@ -107,7 +127,11 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
}
if err = pr . Issue . changeStatus ( sess , doer , true ) ; err != nil {
return fmt . Errorf ( "Pull.changeStatus: %v" , err )
return fmt . Errorf ( "Issue.changeStatus: %v" , err )
}
if err = pr . GetHeadRepo ( ) ; err != nil {
return fmt . Errorf ( "GetHeadRepo: %v" , err )
}
headRepoPath := RepoPath ( pr . HeadUserName , pr . HeadRepo . Name )
@ -150,11 +174,26 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return fmt . Errorf ( "git checkout: %s" , stderr )
}
// Pull commits.
// Add head repo remote.
if _ , stderr , err = process . ExecDir ( - 1 , tmpBasePath ,
fmt . Sprintf ( "PullRequest.Merge(git remote add): %s" , tmpBasePath ) ,
"git" , "remote" , "add" , "head_repo" , headRepoPath ) ; err != nil {
return fmt . Errorf ( "git remote add[%s -> %s]: %s" , headRepoPath , tmpBasePath , stderr )
}
// Merge commits.
if _ , stderr , err = process . ExecDir ( - 1 , tmpBasePath ,
fmt . Sprintf ( "PullRequest.Merge(git fetch): %s" , tmpBasePath ) ,
"git" , "fetch" , "head_repo" ) ; err != nil {
return fmt . Errorf ( "git fetch[%s -> %s]: %s" , headRepoPath , tmpBasePath , stderr )
}
if _ , stderr , err = process . ExecDir ( - 1 , tmpBasePath ,
fmt . Sprintf ( "PullRequest.Merge(git pull): %s" , tmpBasePath ) ,
"git" , "pull" , headRepoPath , pr . HeadBranch ) ; err != nil {
return fmt . Errorf ( "git pull[%s / %s -> %s]: %s" , headRepoPath , pr . HeadBranch , tmpBasePath , stderr )
fmt . Sprintf ( "PullRequest.Merge(git merge): %s" , tmpBasePath ) ,
"git" , "merge" , "--no-ff" , "-m" ,
fmt . Sprintf ( "Merge branch '%s' of %s/%s into %s" , pr . HeadBranch , pr . HeadUserName , pr . HeadRepo . Name , pr . BaseBranch ) ,
"head_repo/" + pr . HeadBranch ) ; err != nil {
return fmt . Errorf ( "git merge[%s]: %s" , tmpBasePath , stderr )
}
// Push back to upstream.
@ -167,6 +206,41 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return sess . Commit ( )
}
// testPatch checks if patch can be merged to base repository without conflit.
func ( pr * PullRequest ) testPatch ( ) ( err error ) {
if pr . BaseRepo == nil {
pr . BaseRepo , err = GetRepositoryByID ( pr . BaseRepoID )
if err != nil {
return fmt . Errorf ( "GetRepositoryByID: %v" , err )
}
}
patchPath , err := pr . BaseRepo . PatchPath ( pr . Index )
if err != nil {
return fmt . Errorf ( "BaseRepo.PatchPath: %v" , err )
}
log . Trace ( "PullRequest[%d].testPatch(patchPath): %s" , pr . ID , patchPath )
if err := pr . BaseRepo . UpdateLocalCopy ( ) ; err != nil {
return fmt . Errorf ( "UpdateLocalCopy: %v" , err )
}
pr . Status = PULL_REQUEST_STATUS_CHECKING
_ , stderr , err := process . ExecDir ( - 1 , pr . BaseRepo . LocalCopyPath ( ) ,
fmt . Sprintf ( "testPatch(git apply --check): %d" , pr . BaseRepo . ID ) ,
"git" , "apply" , "--check" , patchPath )
if err != nil {
if strings . Contains ( stderr , "patch does not apply" ) {
log . Trace ( "PullRequest[%d].testPatch(apply): has conflit" , pr . ID )
pr . Status = PULL_REQUEST_STATUS_CONFLICT
} else {
return fmt . Errorf ( "git apply --check: %v - %s" , err , stderr )
}
}
return nil
}
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest ( repo * Repository , pull * Issue , labelIDs [ ] int64 , uuids [ ] string , pr * PullRequest , patch [ ] byte ) ( err error ) {
sess := x . NewSession ( )
@ -195,32 +269,16 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
return err
}
// Test apply patch.
if err = repo . UpdateLocalCopy ( ) ; err != nil {
return fmt . Errorf ( "UpdateLocalCopy: %v" , err )
if err = repo . SavePatch ( pr . Index , patch ) ; err != nil {
return fmt . Errorf ( "SavePatch: %v" , err )
}
repoPath , err := repo . RepoPath ( )
if err != nil {
return fmt . Errorf ( "RepoPat h: %v" , err )
pr . BaseRepo = repo
if err = pr . testPatch ( ) ; err != nil {
return fmt . Errorf ( "testPatc h: %v" , err )
}
patchPath := path . Join ( repoPath , "pulls" , com . ToStr ( pull . ID ) + ".patch" )
os . MkdirAll ( path . Dir ( patchPath ) , os . ModePerm )
if err = ioutil . WriteFile ( patchPath , patch , 0644 ) ; err != nil {
return fmt . Errorf ( "save patch: %v" , err )
}
if pr . Status == PULL_REQUEST_STATUS_CHECKING {
pr . Status = PULL_REQUEST_STATUS_MERGEABLE
_ , stderr , err := process . ExecDir ( - 1 , repo . LocalCopyPath ( ) ,
fmt . Sprintf ( "NewPullRequest(git apply --check): %d" , repo . ID ) ,
"git" , "apply" , "--check" , patchPath )
if err != nil {
if strings . Contains ( stderr , "patch does not apply" ) {
pr . Status = PULL_REQUEST_STATUS_CONFLICT
} else {
return fmt . Errorf ( "git apply --check: %v - %s" , err , stderr )
}
}
pr . IssueID = pull . ID
@ -236,7 +294,6 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
// by given head/base and repo/branch.
func GetUnmergedPullRequest ( headRepoID , baseRepoID int64 , headBranch , baseBranch string ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := x . Where ( "head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?" ,
headRepoID , headBranch , baseRepoID , baseBranch , false , false ) .
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) . Get ( pr )
@ -249,6 +306,27 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch
return pr , nil
}
// GetUnmergedPullRequestsByHeadInfo returnss all pull requests that are open and has not been merged
// by given head information (repo and branch).
func GetUnmergedPullRequestsByHeadInfo ( headRepoID int64 , headBranch string ) ( [ ] * PullRequest , error ) {
prs := make ( [ ] * PullRequest , 0 , 2 )
return prs , x . Where ( "head_repo_id=? AND head_branch=? AND has_merged=? AND issue.is_closed=?" ,
headRepoID , headBranch , false , false ) .
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) . Find ( & prs )
}
// GetPullRequestByID returns a pull request by given ID.
func GetPullRequestByID ( id int64 ) ( * PullRequest , error ) {
pr := new ( PullRequest )
has , err := x . Id ( id ) . Get ( pr )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { id , 0 , 0 , 0 , "" , "" }
}
return pr , nil
}
// GetPullRequestByIssueID returns pull request by given issue ID.
func GetPullRequestByIssueID ( issueID int64 ) ( * PullRequest , error ) {
pr := & PullRequest {
@ -262,3 +340,144 @@ func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) {
}
return pr , nil
}
// Update updates all fields of pull request.
func ( pr * PullRequest ) Update ( ) error {
_ , err := x . Id ( pr . ID ) . AllCols ( ) . Update ( pr )
return err
}
// Update updates specific fields of pull request.
func ( pr * PullRequest ) UpdateCols ( cols ... string ) error {
_ , err := x . Id ( pr . ID ) . Cols ( cols ... ) . Update ( pr )
return err
}
var PullRequestQueue = NewUniqueQueue ( setting . Repository . PullRequestQueueLength )
// checkAndUpdateStatus checks if pull request is possible to levaing checking status,
// and set to be either conflict or mergeable.
func ( pr * PullRequest ) checkAndUpdateStatus ( ) {
// Status is not changed to conflict means mergeable.
if pr . Status == PULL_REQUEST_STATUS_CHECKING {
pr . Status = PULL_REQUEST_STATUS_MERGEABLE
}
// Make sure there is no waiting test to process before levaing the checking status.
if ! PullRequestQueue . Exist ( pr . ID ) {
if err := pr . UpdateCols ( "status" ) ; err != nil {
log . Error ( 4 , "Update[%d]: %v" , pr . ID , err )
}
}
}
// AddTestPullRequestTask adds new test tasks by given head repository and head branch,
// and generate new patch for testing as needed.
func AddTestPullRequestTask ( headRepoID int64 , headBranch string ) {
log . Trace ( "AddTestPullRequestTask[head_repo_id: %d, head_branch: %s]: finding pull requests" , headRepoID , headBranch )
prs , err := GetUnmergedPullRequestsByHeadInfo ( headRepoID , headBranch )
if err != nil {
log . Error ( 4 , "Find pull requests[head_repo_id: %d, head_branch: %s]: %v" , headRepoID , headBranch , err )
return
}
for _ , pr := range prs {
log . Trace ( "AddTestPullRequestTask[%d]: composing new test task" , pr . ID )
if err := pr . GetHeadRepo ( ) ; err != nil {
log . Error ( 4 , "GetHeadRepo[%d]: %v" , pr . ID , err )
continue
} else if pr . HeadRepo == nil {
log . Trace ( "AddTestPullRequestTask[%d]: ignored cruppted data" , pr . ID )
continue
}
if err := pr . GetBaseRepo ( ) ; err != nil {
log . Error ( 4 , "GetBaseRepo[%d]: %v" , pr . ID , err )
continue
}
headRepoPath , err := pr . HeadRepo . RepoPath ( )
if err != nil {
log . Error ( 4 , "HeadRepo.RepoPath[%d]: %v" , pr . ID , err )
continue
}
headGitRepo , err := git . OpenRepository ( headRepoPath )
if err != nil {
log . Error ( 4 , "OpenRepository[%d]: %v" , pr . ID , err )
continue
}
// Generate patch.
patch , err := headGitRepo . GetPatch ( pr . MergeBase , pr . HeadBranch )
if err != nil {
log . Error ( 4 , "GetPatch[%d]: %v" , pr . ID , err )
continue
}
if err = pr . BaseRepo . SavePatch ( pr . Index , patch ) ; err != nil {
log . Error ( 4 , "BaseRepo.SavePatch[%d]: %v" , pr . ID , err )
continue
}
if ! PullRequestQueue . Exist ( pr . ID ) {
go func ( ) {
PullRequestQueue . Add ( pr . ID )
pr . Status = PULL_REQUEST_STATUS_CHECKING
if err = pr . UpdateCols ( "status" ) ; err != nil {
log . Error ( 5 , "AddTestPullRequestTask.UpdateCols[%d].(add to queue): %v" , pr . ID , err )
}
} ( )
}
}
}
// TestPullRequests checks and tests untested patches of pull requests.
// TODO: test more pull requests at same time.
func TestPullRequests ( ) {
prs := make ( [ ] * PullRequest , 0 , 10 )
x . Iterate ( PullRequest {
Status : PULL_REQUEST_STATUS_CHECKING ,
} ,
func ( idx int , bean interface { } ) error {
pr := bean . ( * PullRequest )
if err := pr . GetBaseRepo ( ) ; err != nil {
log . Error ( 3 , "GetBaseRepo: %v" , err )
return nil
}
if err := pr . testPatch ( ) ; err != nil {
log . Error ( 3 , "testPatch: %v" , err )
return nil
}
prs = append ( prs , pr )
return nil
} )
// Update pull request status.
for _ , pr := range prs {
pr . checkAndUpdateStatus ( )
}
// Start listening on new test requests.
for prID := range PullRequestQueue . Queue ( ) {
log . Trace ( "TestPullRequests[%v]: processing test task" , prID )
PullRequestQueue . Remove ( prID )
pr , err := GetPullRequestByID ( com . StrTo ( prID ) . MustInt64 ( ) )
if err != nil {
log . Error ( 4 , "GetPullRequestByID[%d]: %v" , prID , err )
continue
} else if err = pr . testPatch ( ) ; err != nil {
log . Error ( 4 , "testPatch[%d]: %v" , pr . ID , err )
continue
}
pr . checkAndUpdateStatus ( )
}
}
func InitTestPullRequests ( ) {
go TestPullRequests ( )
}