2019-09-27 02:22:36 +02:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 19:20:29 +01:00
// SPDX-License-Identifier: MIT
2019-09-27 02:22:36 +02:00
package pull
import (
2019-12-15 10:51:28 +01:00
"context"
2019-09-27 02:22:36 +02:00
"fmt"
2022-01-20 00:26:57 +01:00
"io"
2023-01-28 16:54:40 +01:00
"os"
2021-06-25 19:01:43 +02:00
"regexp"
2020-01-09 02:47:45 +01:00
"strings"
2019-09-27 02:22:36 +02:00
"code.gitea.io/gitea/models"
2021-09-19 13:49:59 +02:00
"code.gitea.io/gitea/models/db"
2022-06-12 17:51:54 +02:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 11:37:59 +02:00
issues_model "code.gitea.io/gitea/models/issues"
2021-12-10 02:27:50 +01:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 10:49:20 +01:00
user_model "code.gitea.io/gitea/models/user"
2022-10-12 07:18:26 +02:00
"code.gitea.io/gitea/modules/container"
2019-10-15 05:28:40 +02:00
"code.gitea.io/gitea/modules/git"
2019-12-15 10:51:28 +01:00
"code.gitea.io/gitea/modules/graceful"
2021-07-24 18:03:58 +02:00
"code.gitea.io/gitea/modules/json"
2019-09-27 02:22:36 +02:00
"code.gitea.io/gitea/modules/log"
2019-11-03 21:59:09 +01:00
"code.gitea.io/gitea/modules/notification"
2022-01-20 00:26:57 +01:00
"code.gitea.io/gitea/modules/process"
2022-05-08 18:46:32 +02:00
repo_module "code.gitea.io/gitea/modules/repository"
2020-04-10 13:26:37 +02:00
"code.gitea.io/gitea/modules/setting"
2022-05-04 18:06:23 +02:00
"code.gitea.io/gitea/modules/sync"
2023-01-28 16:54:40 +01:00
"code.gitea.io/gitea/modules/util"
2019-10-28 17:45:43 +01:00
issue_service "code.gitea.io/gitea/services/issue"
2019-09-27 02:22:36 +02:00
)
2022-05-04 18:06:23 +02:00
// TODO: use clustered lock (unique queue? or *abuse* cache)
var pullWorkingPool = sync . NewExclusivePool ( )
2019-09-27 02:22:36 +02:00
// NewPullRequest creates new pull request with labels for repository.
2022-06-13 11:37:59 +02:00
func NewPullRequest ( ctx context . Context , repo * repo_model . Repository , pull * issues_model . Issue , labelIDs [ ] int64 , uuids [ ] string , pr * issues_model . PullRequest , assigneeIDs [ ] int64 ) error {
2019-12-13 23:21:06 +01:00
if err := TestPatch ( pr ) ; err != nil {
return err
}
2022-01-20 00:26:57 +01:00
divergence , err := GetDiverging ( ctx , pr )
2020-04-14 15:53:34 +02:00
if err != nil {
return err
}
pr . CommitsAhead = divergence . Ahead
pr . CommitsBehind = divergence . Behind
2022-06-13 11:37:59 +02:00
if err := issues_model . NewPullRequest ( ctx , repo , pull , labelIDs , uuids , pr ) ; err != nil {
2019-09-27 02:22:36 +02:00
return err
}
2019-10-28 17:45:43 +01:00
for _ , assigneeID := range assigneeIDs {
2023-04-14 20:18:28 +02:00
if err := issue_service . AddAssigneeIfNotAssigned ( ctx , pull , pull . Poster , assigneeID ) ; err != nil {
2019-10-28 17:45:43 +01:00
return err
}
}
2019-09-27 02:22:36 +02:00
pr . Issue = pull
pull . PullRequest = pr
2019-11-03 21:59:09 +01:00
2022-01-20 00:26:57 +01:00
// Now - even if the request context has been cancelled as the PR has been created
// in the db and there is no way to cancel that transaction we have to proceed - therefore
// create new context and work from there
prCtx , _ , finished := process . GetManager ( ) . AddContext ( graceful . GetManager ( ) . HammerContext ( ) , fmt . Sprintf ( "NewPullRequest: %s:%d" , repo . FullName ( ) , pr . Index ) )
defer finished ( )
2022-06-13 11:37:59 +02:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2022-01-20 00:26:57 +01:00
err = PushToBaseRepo ( prCtx , pr )
2021-07-28 11:42:56 +02:00
} else {
2022-01-20 00:26:57 +01:00
err = UpdateRef ( prCtx , pr )
2021-07-28 11:42:56 +02:00
}
if err != nil {
2019-12-15 04:28:51 +01:00
return err
}
2022-06-13 11:37:59 +02:00
mentions , err := issues_model . FindAndUpdateIssueMentions ( ctx , pull , pull . Poster , pull . Content )
2021-01-02 18:04:02 +01:00
if err != nil {
return err
}
2022-11-19 09:12:33 +01:00
notification . NotifyNewPullRequest ( prCtx , pr , mentions )
2021-01-17 15:15:57 +01:00
if len ( pull . Labels ) > 0 {
2022-11-19 09:12:33 +01:00
notification . NotifyIssueChangeLabels ( prCtx , pull . Poster , pull , pull . Labels , nil )
2021-01-17 15:15:57 +01:00
}
if pull . Milestone != nil {
2022-11-19 09:12:33 +01:00
notification . NotifyIssueChangeMilestone ( prCtx , pull . Poster , pull , 0 )
2021-01-17 15:15:57 +01:00
}
2019-09-27 02:22:36 +02:00
2020-05-20 14:47:24 +02:00
// add first push codes comment
2022-03-29 21:13:41 +02:00
baseGitRepo , err := git . OpenRepository ( prCtx , pr . BaseRepo . RepoPath ( ) )
2020-05-20 14:47:24 +02:00
if err != nil {
return err
}
defer baseGitRepo . Close ( )
compareInfo , err := baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) ,
2022-01-18 08:45:43 +01:00
git . BranchPrefix + pr . BaseBranch , pr . GetGitRefName ( ) , false , false )
2020-05-20 14:47:24 +02:00
if err != nil {
return err
}
2021-08-09 20:08:51 +02:00
if len ( compareInfo . Commits ) > 0 {
2022-06-13 11:37:59 +02:00
data := issues_model . PushActionContent { IsForcePush : false }
2021-08-09 20:08:51 +02:00
data . CommitIDs = make ( [ ] string , 0 , len ( compareInfo . Commits ) )
for i := len ( compareInfo . Commits ) - 1 ; i >= 0 ; i -- {
data . CommitIDs = append ( data . CommitIDs , compareInfo . Commits [ i ] . ID . String ( ) )
2020-05-20 14:47:24 +02:00
}
dataJSON , err := json . Marshal ( data )
if err != nil {
return err
}
2022-06-13 11:37:59 +02:00
ops := & issues_model . CreateCommentOptions {
Type : issues_model . CommentTypePullRequestPush ,
2020-05-20 14:47:24 +02:00
Doer : pull . Poster ,
Repo : repo ,
Issue : pr . Issue ,
IsForcePush : false ,
Content : string ( dataJSON ) ,
}
2023-04-14 20:18:28 +02:00
_ , _ = issue_service . CreateComment ( ctx , ops )
2023-06-08 10:56:05 +02:00
if ! pr . IsWorkInProgress ( ) {
if err := issues_model . PullRequestCodeOwnersReview ( ctx , pull , pr ) ; err != nil {
return err
}
}
2020-05-20 14:47:24 +02:00
}
2019-09-27 02:22:36 +02:00
return nil
}
2019-10-15 05:28:40 +02:00
2019-12-16 07:20:25 +01:00
// ChangeTargetBranch changes the target branch of this pull request, as the given user.
2022-06-13 11:37:59 +02:00
func ChangeTargetBranch ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User , targetBranch string ) ( err error ) {
2022-05-04 18:06:23 +02:00
pullWorkingPool . CheckIn ( fmt . Sprint ( pr . ID ) )
defer pullWorkingPool . CheckOut ( fmt . Sprint ( pr . ID ) )
2019-12-16 07:20:25 +01:00
// Current target branch is already the same
if pr . BaseBranch == targetBranch {
return nil
}
if pr . Issue . IsClosed {
2022-06-13 11:37:59 +02:00
return issues_model . ErrIssueIsClosed {
2019-12-16 07:20:25 +01:00
ID : pr . Issue . ID ,
RepoID : pr . Issue . RepoID ,
Index : pr . Issue . Index ,
}
}
if pr . HasMerged {
return models . ErrPullRequestHasMerged {
ID : pr . ID ,
IssueID : pr . Index ,
HeadRepoID : pr . HeadRepoID ,
BaseRepoID : pr . BaseRepoID ,
HeadBranch : pr . HeadBranch ,
BaseBranch : pr . BaseBranch ,
}
}
// Check if branches are equal
2022-01-20 00:26:57 +01:00
branchesEqual , err := IsHeadEqualWithBranch ( ctx , pr , targetBranch )
2019-12-16 07:20:25 +01:00
if err != nil {
return err
}
if branchesEqual {
2023-06-29 12:03:20 +02:00
return git_model . ErrBranchesEqual {
2019-12-16 07:20:25 +01:00
HeadBranchName : pr . HeadBranch ,
BaseBranchName : targetBranch ,
}
}
// Check if pull request for the new target branch already exists
2022-11-19 09:12:33 +01:00
existingPr , err := issues_model . GetUnmergedPullRequest ( ctx , pr . HeadRepoID , pr . BaseRepoID , pr . HeadBranch , targetBranch , issues_model . PullRequestFlowGithub )
2019-12-16 07:20:25 +01:00
if existingPr != nil {
2022-06-13 11:37:59 +02:00
return issues_model . ErrPullRequestAlreadyExists {
2019-12-16 07:20:25 +01:00
ID : existingPr . ID ,
IssueID : existingPr . Index ,
HeadRepoID : existingPr . HeadRepoID ,
BaseRepoID : existingPr . BaseRepoID ,
HeadBranch : existingPr . HeadBranch ,
BaseBranch : existingPr . BaseBranch ,
}
}
2022-06-13 11:37:59 +02:00
if err != nil && ! issues_model . IsErrPullRequestNotExist ( err ) {
2019-12-16 07:20:25 +01:00
return err
}
// Set new target branch
oldBranch := pr . BaseBranch
pr . BaseBranch = targetBranch
// Refresh patch
if err := TestPatch ( pr ) ; err != nil {
return err
}
// Update target branch, PR diff and status
// This is the same as checkAndUpdateStatus in check service, but also updates base_branch
2022-06-13 11:37:59 +02:00
if pr . Status == issues_model . PullRequestStatusChecking {
pr . Status = issues_model . PullRequestStatusMergeable
2019-12-16 07:20:25 +01:00
}
2020-06-16 19:52:33 +02:00
// Update Commit Divergence
2022-01-20 00:26:57 +01:00
divergence , err := GetDiverging ( ctx , pr )
2020-06-16 19:52:33 +02:00
if err != nil {
return err
}
pr . CommitsAhead = divergence . Ahead
pr . CommitsBehind = divergence . Behind
2022-11-19 09:12:33 +01:00
if err := pr . UpdateColsIfNotMerged ( ctx , "merge_base" , "status" , "conflicted_files" , "changed_protected_files" , "base_branch" , "commits_ahead" , "commits_behind" ) ; err != nil {
2019-12-16 07:20:25 +01:00
return err
}
// Create comment
2022-06-13 11:37:59 +02:00
options := & issues_model . CreateCommentOptions {
Type : issues_model . CommentTypeChangeTargetBranch ,
2019-12-16 07:20:25 +01:00
Doer : doer ,
Repo : pr . Issue . Repo ,
Issue : pr . Issue ,
OldRef : oldBranch ,
NewRef : targetBranch ,
}
2023-04-14 20:18:28 +02:00
if _ , err = issue_service . CreateComment ( ctx , options ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "CreateChangeTargetBranchComment: %w" , err )
2019-12-16 07:20:25 +01:00
}
return nil
}
2022-06-13 11:37:59 +02:00
func checkForInvalidation ( ctx context . Context , requests issues_model . PullRequestList , repoID int64 , doer * user_model . User , branch string ) error {
2022-12-03 03:48:26 +01:00
repo , err := repo_model . GetRepositoryByID ( ctx , repoID )
2019-10-15 05:28:40 +02:00
if err != nil {
2022-11-19 09:12:33 +01:00
return fmt . Errorf ( "GetRepositoryByIDCtx: %w" , err )
2019-10-15 05:28:40 +02:00
}
2022-03-29 21:13:41 +02:00
gitRepo , err := git . OpenRepository ( ctx , repo . RepoPath ( ) )
2019-10-15 05:28:40 +02:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "git.OpenRepository: %w" , err )
2019-10-15 05:28:40 +02:00
}
go func ( ) {
2019-12-15 10:51:28 +01:00
// FIXME: graceful: We need to tell the manager we're doing something...
2023-01-17 22:03:44 +01:00
err := InvalidateCodeComments ( ctx , requests , doer , gitRepo , branch )
2019-10-15 05:28:40 +02:00
if err != nil {
log . Error ( "PullRequestList.InvalidateCodeComments: %v" , err )
}
2019-11-13 08:01:19 +01:00
gitRepo . Close ( )
2019-10-15 05:28:40 +02:00
} ( )
return nil
}
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed.
2021-11-24 10:49:20 +01:00
func AddTestPullRequestTask ( doer * user_model . User , repoID int64 , branch string , isSync bool , oldCommitID , newCommitID string ) {
2019-10-15 05:28:40 +02:00
log . Trace ( "AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests" , repoID , branch )
2019-12-15 10:51:28 +01:00
graceful . GetManager ( ) . RunWithShutdownContext ( func ( ctx context . Context ) {
// There is no sensible way to shut this down ":-("
// If you don't let it run all the way then you will lose data
2022-05-07 19:05:52 +02:00
// TODO: graceful: AddTestPullRequestTask needs to become a queue!
2019-10-15 05:28:40 +02:00
2023-05-08 08:39:32 +02:00
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( repoID , branch )
2019-12-15 10:51:28 +01:00
if err != nil {
log . Error ( "Find pull requests [head_repo_id: %d, head_branch: %s]: %v" , repoID , branch , err )
return
2019-10-15 05:28:40 +02:00
}
2019-12-15 10:51:28 +01:00
2023-02-05 12:57:38 +01:00
for _ , pr := range prs {
log . Trace ( "Updating PR[%d]: composing new test task" , pr . ID )
if pr . Flow == issues_model . PullRequestFlowGithub {
if err := PushToBaseRepo ( ctx , pr ) ; err != nil {
log . Error ( "PushToBaseRepo: %v" , err )
continue
}
} else {
continue
}
AddToTaskQueue ( pr )
comment , err := CreatePushPullComment ( ctx , doer , pr , oldCommitID , newCommitID )
2023-05-08 08:39:32 +02:00
if err == nil && comment != nil {
2023-02-05 12:57:38 +01:00
notification . NotifyPullRequestPushCommits ( ctx , doer , pr , comment )
}
}
2019-12-15 10:51:28 +01:00
if isSync {
2022-06-13 11:37:59 +02:00
requests := issues_model . PullRequestList ( prs )
2019-12-15 10:51:28 +01:00
if err = requests . LoadAttributes ( ) ; err != nil {
log . Error ( "PullRequestList.LoadAttributes: %v" , err )
}
2022-01-20 00:26:57 +01:00
if invalidationErr := checkForInvalidation ( ctx , requests , repoID , doer , branch ) ; invalidationErr != nil {
2019-12-15 10:51:28 +01:00
log . Error ( "checkForInvalidation: %v" , invalidationErr )
}
if err == nil {
for _ , pr := range prs {
2020-01-09 02:47:45 +01:00
if newCommitID != "" && newCommitID != git . EmptySHA {
2022-01-20 00:26:57 +01:00
changed , err := checkIfPRContentChanged ( ctx , pr , oldCommitID , newCommitID )
2020-01-09 02:47:45 +01:00
if err != nil {
log . Error ( "checkIfPRContentChanged: %v" , err )
}
if changed {
// Mark old reviews as stale if diff to mergebase has changed
2022-06-13 11:37:59 +02:00
if err := issues_model . MarkReviewsAsStale ( pr . IssueID ) ; err != nil {
2020-01-09 02:47:45 +01:00
log . Error ( "MarkReviewsAsStale: %v" , err )
}
2023-07-20 09:18:52 +02:00
// dismiss all approval reviews if protected branch rule item enabled.
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , pr . BaseRepoID , pr . BaseBranch )
if err != nil {
log . Error ( "GetFirstMatchProtectedBranchRule: %v" , err )
}
if pb != nil && pb . DismissStaleApprovals {
if err := DismissApprovalReviews ( ctx , doer , pr ) ; err != nil {
log . Error ( "DismissApprovalReviews: %v" , err )
}
}
2020-01-09 02:47:45 +01:00
}
2022-06-13 11:37:59 +02:00
if err := issues_model . MarkReviewsAsNotStale ( pr . IssueID , newCommitID ) ; err != nil {
2020-01-09 02:47:45 +01:00
log . Error ( "MarkReviewsAsNotStale: %v" , err )
}
2022-01-20 00:26:57 +01:00
divergence , err := GetDiverging ( ctx , pr )
2020-04-14 15:53:34 +02:00
if err != nil {
log . Error ( "GetDiverging: %v" , err )
} else {
2022-05-20 16:08:52 +02:00
err = pr . UpdateCommitDivergence ( ctx , divergence . Ahead , divergence . Behind )
2020-04-14 15:53:34 +02:00
if err != nil {
log . Error ( "UpdateCommitDivergence: %v" , err )
}
}
2020-01-09 02:47:45 +01:00
}
2022-11-19 09:12:33 +01:00
notification . NotifyPullRequestSynchronized ( ctx , doer , pr )
2019-12-15 10:51:28 +01:00
}
2019-10-15 05:28:40 +02:00
}
}
2019-12-15 10:51:28 +01:00
log . Trace ( "AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests" , repoID , branch )
2022-06-13 11:37:59 +02:00
prs , err = issues_model . GetUnmergedPullRequestsByBaseInfo ( repoID , branch )
2019-12-15 10:51:28 +01:00
if err != nil {
log . Error ( "Find pull requests [base_repo_id: %d, base_branch: %s]: %v" , repoID , branch , err )
return
}
for _ , pr := range prs {
2022-01-20 00:26:57 +01:00
divergence , err := GetDiverging ( ctx , pr )
2020-04-14 15:53:34 +02:00
if err != nil {
2023-06-29 12:03:20 +02:00
if git_model . IsErrBranchNotExist ( err ) && ! git . IsBranchExist ( ctx , pr . HeadRepo . RepoPath ( ) , pr . HeadBranch ) {
2021-07-13 01:26:25 +02:00
log . Warn ( "Cannot test PR %s/%d: head_branch %s no longer exists" , pr . BaseRepo . Name , pr . IssueID , pr . HeadBranch )
} else {
log . Error ( "GetDiverging: %v" , err )
}
2020-04-14 15:53:34 +02:00
} else {
2022-05-20 16:08:52 +02:00
err = pr . UpdateCommitDivergence ( ctx , divergence . Ahead , divergence . Behind )
2020-04-14 15:53:34 +02:00
if err != nil {
log . Error ( "UpdateCommitDivergence: %v" , err )
}
}
2019-12-15 10:51:28 +01:00
AddToTaskQueue ( pr )
}
} )
2019-10-15 05:28:40 +02:00
}
2019-12-15 04:28:51 +01:00
2020-01-09 02:47:45 +01:00
// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
2022-06-13 11:37:59 +02:00
func checkIfPRContentChanged ( ctx context . Context , pr * issues_model . PullRequest , oldCommitID , newCommitID string ) ( hasChanged bool , err error ) {
2023-03-07 21:07:35 +01:00
prCtx , cancel , err := createTemporaryRepoForPR ( ctx , pr )
2020-01-09 02:47:45 +01:00
if err != nil {
2023-03-07 21:07:35 +01:00
log . Error ( "CreateTemporaryRepoForPR %-v: %v" , pr , err )
2023-01-28 16:54:40 +01:00
return false , err
2020-01-09 02:47:45 +01:00
}
2023-03-07 21:07:35 +01:00
defer cancel ( )
2023-01-28 16:54:40 +01:00
2023-03-07 21:07:35 +01:00
tmpRepo , err := git . OpenRepository ( ctx , prCtx . tmpBasePath )
2020-01-09 02:47:45 +01:00
if err != nil {
2023-01-28 16:54:40 +01:00
return false , fmt . Errorf ( "OpenRepository: %w" , err )
2020-01-09 02:47:45 +01:00
}
2023-01-28 16:54:40 +01:00
defer tmpRepo . Close ( )
2020-01-09 02:47:45 +01:00
2023-01-28 16:54:40 +01:00
// Find the merge-base
_ , base , err := tmpRepo . GetMergeBase ( "" , "base" , "tracking" )
if err != nil {
return false , fmt . Errorf ( "GetMergeBase: %w" , err )
2020-01-09 02:47:45 +01:00
}
2023-01-28 16:54:40 +01:00
cmd := git . NewCommand ( ctx , "diff" , "--name-only" , "-z" ) . AddDynamicArguments ( newCommitID , oldCommitID , base )
stdoutReader , stdoutWriter , err := os . Pipe ( )
if err != nil {
return false , fmt . Errorf ( "unable to open pipe for to run diff: %w" , err )
}
if err := cmd . Run ( & git . RunOpts {
2023-03-07 21:07:35 +01:00
Dir : prCtx . tmpBasePath ,
2023-01-28 16:54:40 +01:00
Stdout : stdoutWriter ,
PipelineFunc : func ( ctx context . Context , cancel context . CancelFunc ) error {
_ = stdoutWriter . Close ( )
defer func ( ) {
_ = stdoutReader . Close ( )
} ( )
return util . IsEmptyReader ( stdoutReader )
} ,
} ) ; err != nil {
if err == util . ErrNotEmpty {
2020-01-09 02:47:45 +01:00
return true , nil
}
2023-01-28 16:54:40 +01:00
log . Error ( "Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v" ,
newCommitID , oldCommitID , base ,
pr . ID , pr . BaseRepo . FullName ( ) , pr . BaseBranch , pr . HeadRepo . FullName ( ) , pr . HeadBranch ,
err )
return false , fmt . Errorf ( "Unable to run git diff --name-only -z %s %s %s: %w" , newCommitID , oldCommitID , base , err )
2020-01-09 02:47:45 +01:00
}
return false , nil
}
2019-12-15 04:28:51 +01:00
// PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
2022-06-13 11:37:59 +02:00
func PushToBaseRepo ( ctx context . Context , pr * issues_model . PullRequest ) ( err error ) {
2022-01-20 00:26:57 +01:00
return pushToBaseRepoHelper ( ctx , pr , "" )
2021-06-23 23:08:26 +02:00
}
2022-06-13 11:37:59 +02:00
func pushToBaseRepoHelper ( ctx context . Context , pr * issues_model . PullRequest , prefixHeadBranch string ) ( err error ) {
2019-12-15 04:28:51 +01:00
log . Trace ( "PushToBaseRepo[%d]: pushing commits to base repo '%s'" , pr . BaseRepoID , pr . GetGitRefName ( ) )
2022-11-19 09:12:33 +01:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-02-21 19:18:13 +01:00
log . Error ( "Unable to load head repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2019-12-15 04:28:51 +01:00
headRepoPath := pr . HeadRepo . RepoPath ( )
2020-01-28 11:23:58 +01:00
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-02-21 19:18:13 +01:00
log . Error ( "Unable to load base repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2020-09-15 05:32:31 +02:00
baseRepoPath := pr . BaseRepo . RepoPath ( )
2019-12-15 04:28:51 +01:00
2022-11-19 09:12:33 +01:00
if err = pr . LoadIssue ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "unable to load issue %d for pr %d: %w" , pr . IssueID , pr . ID , err )
2019-12-27 22:15:04 +01:00
}
2022-11-19 09:12:33 +01:00
if err = pr . Issue . LoadPoster ( ctx ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "unable to load poster %d for pr %d: %w" , pr . Issue . PosterID , pr . ID , err )
2019-12-27 22:15:04 +01:00
}
2020-09-15 05:32:31 +02:00
gitRefName := pr . GetGitRefName ( )
2022-01-20 00:26:57 +01:00
if err := git . Push ( ctx , headRepoPath , git . PushOptions {
2020-09-15 05:32:31 +02:00
Remote : baseRepoPath ,
2021-06-23 23:08:26 +02:00
Branch : prefixHeadBranch + pr . HeadBranch + ":" + gitRefName ,
2019-12-15 04:28:51 +01:00
Force : true ,
2019-12-27 22:15:04 +01:00
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
2022-05-08 18:46:32 +02:00
Env : repo_module . InternalPushingEnvironment ( pr . Issue . Poster , pr . BaseRepo ) ,
2019-12-15 04:28:51 +01:00
} ) ; err != nil {
2020-06-08 20:07:41 +02:00
if git . IsErrPushOutOfDate ( err ) {
// This should not happen as we're using force!
2020-09-15 05:32:31 +02:00
log . Error ( "Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v" , pr . BaseRepo . FullName ( ) , pr . Index , pr . BaseRepo , gitRefName , err )
2020-06-08 20:07:41 +02:00
return err
} else if git . IsErrPushRejected ( err ) {
rejectErr := err . ( * git . ErrPushRejected )
2020-09-15 05:32:31 +02:00
log . Info ( "Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v" , pr . BaseRepo . FullName ( ) , pr . Index , pr . BaseRepo , gitRefName , rejectErr . StdOut , rejectErr . StdErr , rejectErr . Err )
2020-06-08 20:07:41 +02:00
return err
2021-06-23 23:08:26 +02:00
} else if git . IsErrMoreThanOne ( err ) {
if prefixHeadBranch != "" {
log . Info ( "Can't push with %s%s" , prefixHeadBranch , pr . HeadBranch )
return err
}
2022-01-20 00:26:57 +01:00
log . Info ( "Retrying to push with %s%s" , git . BranchPrefix , pr . HeadBranch )
err = pushToBaseRepoHelper ( ctx , pr , git . BranchPrefix )
2021-06-23 23:08:26 +02:00
return err
2020-06-08 20:07:41 +02:00
}
2020-09-15 05:32:31 +02:00
log . Error ( "Unable to push PR head for %s#%d (%-v:%s) due to Error: %v" , pr . BaseRepo . FullName ( ) , pr . Index , pr . BaseRepo , gitRefName , err )
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "Push: %s:%s %s:%s %w" , pr . HeadRepo . FullName ( ) , pr . HeadBranch , pr . BaseRepo . FullName ( ) , gitRefName , err )
2019-12-15 04:28:51 +01:00
}
return nil
}
2020-01-25 03:48:22 +01:00
2021-07-28 11:42:56 +02:00
// UpdateRef update refs/pull/id/head directly for agit flow pull request
2022-06-13 11:37:59 +02:00
func UpdateRef ( ctx context . Context , pr * issues_model . PullRequest ) ( err error ) {
2021-07-28 11:42:56 +02:00
log . Trace ( "UpdateRef[%d]: upgate pull request ref in base repo '%s'" , pr . ID , pr . GetGitRefName ( ) )
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2021-07-28 11:42:56 +02:00
log . Error ( "Unable to load base repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2022-10-23 16:44:45 +02:00
_ , _ , err = git . NewCommand ( ctx , "update-ref" ) . AddDynamicArguments ( pr . GetGitRefName ( ) , pr . HeadCommitID ) . RunStdString ( & git . RunOpts { Dir : pr . BaseRepo . RepoPath ( ) } )
2021-07-28 11:42:56 +02:00
if err != nil {
log . Error ( "Unable to update ref in base repository for PR[%d] Error: %v" , pr . ID , err )
}
return err
}
2020-01-25 03:48:22 +01:00
type errlist [ ] error
func ( errs errlist ) Error ( ) string {
if len ( errs ) > 0 {
var buf strings . Builder
for i , err := range errs {
if i > 0 {
buf . WriteString ( ", " )
}
buf . WriteString ( err . Error ( ) )
}
return buf . String ( )
}
return ""
}
// CloseBranchPulls close all the pull requests who's head branch is the branch
2021-11-24 10:49:20 +01:00
func CloseBranchPulls ( doer * user_model . User , repoID int64 , branch string ) error {
2023-05-08 08:39:32 +02:00
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( repoID , branch )
2020-01-25 03:48:22 +01:00
if err != nil {
return err
}
2022-06-13 11:37:59 +02:00
prs2 , err := issues_model . GetUnmergedPullRequestsByBaseInfo ( repoID , branch )
2020-01-25 03:48:22 +01:00
if err != nil {
return err
}
prs = append ( prs , prs2 ... )
2022-06-13 11:37:59 +02:00
if err := issues_model . PullRequestList ( prs ) . LoadAttributes ( ) ; err != nil {
2020-01-25 03:48:22 +01:00
return err
}
var errs errlist
for _ , pr := range prs {
2023-01-25 05:47:53 +01:00
if err = issue_service . ChangeStatus ( pr . Issue , doer , "" , true ) ; err != nil && ! issues_model . IsErrPullWasClosed ( err ) && ! issues_model . IsErrDependenciesLeft ( err ) {
2020-01-25 03:48:22 +01:00
errs = append ( errs , err )
}
}
if len ( errs ) > 0 {
return errs
}
return nil
}
2021-03-01 18:39:44 +01:00
// CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository
2022-01-20 00:26:57 +01:00
func CloseRepoBranchesPulls ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository ) error {
branches , _ , err := git . GetBranchesByPath ( ctx , repo . RepoPath ( ) , 0 , 0 )
2020-01-25 03:48:22 +01:00
if err != nil {
return err
}
var errs errlist
for _ , branch := range branches {
2023-05-08 08:39:32 +02:00
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( repo . ID , branch . Name )
2020-01-25 03:48:22 +01:00
if err != nil {
return err
}
2022-06-13 11:37:59 +02:00
if err = issues_model . PullRequestList ( prs ) . LoadAttributes ( ) ; err != nil {
2020-01-25 03:48:22 +01:00
return err
}
for _ , pr := range prs {
2021-03-01 18:39:44 +01:00
// If the base repository for this pr is this repository there is no need to close it
// as it is going to be deleted anyway
if pr . BaseRepoID == repo . ID {
continue
}
2023-01-25 05:47:53 +01:00
if err = issue_service . ChangeStatus ( pr . Issue , doer , "" , true ) ; err != nil && ! issues_model . IsErrPullWasClosed ( err ) {
2020-01-25 03:48:22 +01:00
errs = append ( errs , err )
}
}
}
if len ( errs ) > 0 {
return errs
}
return nil
}
2020-04-10 13:26:37 +02:00
2021-06-25 19:01:43 +02:00
var commitMessageTrailersPattern = regexp . MustCompile ( ` (?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$ ` )
2020-12-21 17:46:14 +01:00
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
2022-06-13 11:37:59 +02:00
func GetSquashMergeCommitMessages ( ctx context . Context , pr * issues_model . PullRequest ) string {
2022-11-19 09:12:33 +01:00
if err := pr . LoadIssue ( ctx ) ; err != nil {
2020-04-10 13:26:37 +02:00
log . Error ( "Cannot load issue %d for PR id %d: Error: %v" , pr . IssueID , pr . ID , err )
return ""
}
2022-11-19 09:12:33 +01:00
if err := pr . Issue . LoadPoster ( ctx ) ; err != nil {
2020-04-10 13:26:37 +02:00
log . Error ( "Cannot load poster %d for pr id %d, index %d Error: %v" , pr . Issue . PosterID , pr . ID , pr . Index , err )
return ""
}
if pr . HeadRepo == nil {
var err error
2022-12-03 03:48:26 +01:00
pr . HeadRepo , err = repo_model . GetRepositoryByID ( ctx , pr . HeadRepoID )
2020-04-10 13:26:37 +02:00
if err != nil {
2022-11-19 09:12:33 +01:00
log . Error ( "GetRepositoryByIdCtx[%d]: %v" , pr . HeadRepoID , err )
2020-04-10 13:26:37 +02:00
return ""
}
}
2022-01-20 00:26:57 +01:00
gitRepo , closer , err := git . RepositoryFromContextOrOpen ( ctx , pr . HeadRepo . RepoPath ( ) )
2020-04-10 13:26:37 +02:00
if err != nil {
log . Error ( "Unable to open head repository: Error: %v" , err )
return ""
}
2022-01-20 00:26:57 +01:00
defer closer . Close ( )
2020-04-10 13:26:37 +02:00
2021-07-28 11:42:56 +02:00
var headCommit * git . Commit
2022-06-13 11:37:59 +02:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2021-07-28 11:42:56 +02:00
headCommit , err = gitRepo . GetBranchCommit ( pr . HeadBranch )
} else {
pr . HeadCommitID , err = gitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if err != nil {
log . Error ( "Unable to get head commit: %s Error: %v" , pr . GetGitRefName ( ) , err )
return ""
}
headCommit , err = gitRepo . GetCommit ( pr . HeadCommitID )
}
2020-04-10 13:26:37 +02:00
if err != nil {
log . Error ( "Unable to get head commit: %s Error: %v" , pr . HeadBranch , err )
return ""
}
mergeBase , err := gitRepo . GetCommit ( pr . MergeBase )
if err != nil {
log . Error ( "Unable to get merge base commit: %s Error: %v" , pr . MergeBase , err )
return ""
}
limit := setting . Repository . PullRequest . DefaultMergeMessageCommitsLimit
2021-08-09 20:08:51 +02:00
commits , err := gitRepo . CommitsBetweenLimit ( headCommit , mergeBase , limit , 0 )
2020-04-10 13:26:37 +02:00
if err != nil {
log . Error ( "Unable to get commits between: %s %s Error: %v" , pr . HeadBranch , pr . MergeBase , err )
return ""
}
posterSig := pr . Issue . Poster . NewGitSig ( ) . String ( )
2022-10-12 07:18:26 +02:00
uniqueAuthors := make ( container . Set [ string ] )
2021-08-09 20:08:51 +02:00
authors := make ( [ ] string , 0 , len ( commits ) )
2020-04-10 13:26:37 +02:00
stringBuilder := strings . Builder { }
2020-12-21 17:46:14 +01:00
2021-06-19 00:08:22 +02:00
if ! setting . Repository . PullRequest . PopulateSquashCommentWithCommitMessages {
2021-06-25 19:01:43 +02:00
message := strings . TrimSpace ( pr . Issue . Content )
stringBuilder . WriteString ( message )
2021-06-19 00:08:22 +02:00
if stringBuilder . Len ( ) > 0 {
stringBuilder . WriteRune ( '\n' )
2021-06-25 19:01:43 +02:00
if ! commitMessageTrailersPattern . MatchString ( message ) {
stringBuilder . WriteRune ( '\n' )
}
2021-06-19 00:08:22 +02:00
}
2020-12-21 17:46:14 +01:00
}
2020-11-25 21:08:17 +01:00
// commits list is in reverse chronological order
2021-08-09 20:08:51 +02:00
first := true
for i := len ( commits ) - 1 ; i >= 0 ; i -- {
commit := commits [ i ]
2021-06-19 00:08:22 +02:00
if setting . Repository . PullRequest . PopulateSquashCommentWithCommitMessages {
maxSize := setting . Repository . PullRequest . DefaultMergeMessageSize
if maxSize < 0 || stringBuilder . Len ( ) < maxSize {
var toWrite [ ] byte
2021-08-09 20:08:51 +02:00
if first {
first = false
2021-06-19 00:08:22 +02:00
toWrite = [ ] byte ( strings . TrimPrefix ( commit . CommitMessage , pr . Issue . Title ) )
} else {
toWrite = [ ] byte ( commit . CommitMessage )
}
if len ( toWrite ) > maxSize - stringBuilder . Len ( ) && maxSize > - 1 {
toWrite = append ( toWrite [ : maxSize - stringBuilder . Len ( ) ] , "..." ... )
}
if _ , err := stringBuilder . Write ( toWrite ) ; err != nil {
log . Error ( "Unable to write commit message Error: %v" , err )
return ""
}
if _ , err := stringBuilder . WriteRune ( '\n' ) ; err != nil {
log . Error ( "Unable to write commit message Error: %v" , err )
return ""
}
}
}
2020-04-10 13:26:37 +02:00
authorString := commit . Author . String ( )
2022-10-12 07:18:26 +02:00
if uniqueAuthors . Add ( authorString ) && authorString != posterSig {
2023-03-10 04:17:04 +01:00
// Compare use account as well to avoid adding the same author multiple times
// times when email addresses are private or multiple emails are used.
commitUser , _ := user_model . GetUserByEmail ( ctx , commit . Author . Email )
if commitUser == nil || commitUser . ID != pr . Issue . Poster . ID {
authors = append ( authors , authorString )
}
2020-04-10 13:26:37 +02:00
}
}
// Consider collecting the remaining authors
if limit >= 0 && setting . Repository . PullRequest . DefaultMergeMessageAllAuthors {
skip := limit
limit = 30
for {
2021-08-09 20:08:51 +02:00
commits , err := gitRepo . CommitsBetweenLimit ( headCommit , mergeBase , limit , skip )
2020-04-10 13:26:37 +02:00
if err != nil {
log . Error ( "Unable to get commits between: %s %s Error: %v" , pr . HeadBranch , pr . MergeBase , err )
return ""
}
2021-08-09 20:08:51 +02:00
if len ( commits ) == 0 {
2020-04-10 13:26:37 +02:00
break
}
2021-08-09 20:08:51 +02:00
for _ , commit := range commits {
2020-04-10 13:26:37 +02:00
authorString := commit . Author . String ( )
2022-10-12 07:18:26 +02:00
if uniqueAuthors . Add ( authorString ) && authorString != posterSig {
2023-03-10 04:17:04 +01:00
commitUser , _ := user_model . GetUserByEmail ( ctx , commit . Author . Email )
if commitUser == nil || commitUser . ID != pr . Issue . Poster . ID {
authors = append ( authors , authorString )
}
2020-04-10 13:26:37 +02:00
}
}
2020-11-27 21:00:52 +01:00
skip += limit
2020-04-10 13:26:37 +02:00
}
}
for _ , author := range authors {
if _ , err := stringBuilder . Write ( [ ] byte ( "Co-authored-by: " ) ) ; err != nil {
log . Error ( "Unable to write to string builder Error: %v" , err )
return ""
}
if _ , err := stringBuilder . Write ( [ ] byte ( author ) ) ; err != nil {
log . Error ( "Unable to write to string builder Error: %v" , err )
return ""
}
if _ , err := stringBuilder . WriteRune ( '\n' ) ; err != nil {
log . Error ( "Unable to write to string builder Error: %v" , err )
return ""
}
}
return stringBuilder . String ( )
}
2022-04-27 00:40:01 +02:00
// GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
2022-06-13 11:37:59 +02:00
func GetIssuesLastCommitStatus ( ctx context . Context , issues issues_model . IssueList ) ( map [ int64 ] * git_model . CommitStatus , error ) {
2022-04-27 00:40:01 +02:00
_ , lastStatus , err := GetIssuesAllCommitStatus ( ctx , issues )
return lastStatus , err
}
// GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
2022-06-13 11:37:59 +02:00
func GetIssuesAllCommitStatus ( ctx context . Context , issues issues_model . IssueList ) ( map [ int64 ] [ ] * git_model . CommitStatus , map [ int64 ] * git_model . CommitStatus , error ) {
2022-11-19 09:12:33 +01:00
if err := issues . LoadPullRequests ( ctx ) ; err != nil {
2022-04-27 00:40:01 +02:00
return nil , nil , err
2021-04-15 19:34:43 +02:00
}
2022-11-19 09:12:33 +01:00
if _ , err := issues . LoadRepositories ( ctx ) ; err != nil {
2022-04-27 00:40:01 +02:00
return nil , nil , err
2021-04-15 19:34:43 +02:00
}
var (
gitRepos = make ( map [ int64 ] * git . Repository )
2022-06-12 17:51:54 +02:00
res = make ( map [ int64 ] [ ] * git_model . CommitStatus )
lastRes = make ( map [ int64 ] * git_model . CommitStatus )
2021-04-15 19:34:43 +02:00
err error
)
defer func ( ) {
for _ , gitRepo := range gitRepos {
gitRepo . Close ( )
}
} ( )
for _ , issue := range issues {
if ! issue . IsPull {
continue
}
gitRepo , ok := gitRepos [ issue . RepoID ]
if ! ok {
2022-03-29 21:13:41 +02:00
gitRepo , err = git . OpenRepository ( ctx , issue . Repo . RepoPath ( ) )
2021-04-15 19:34:43 +02:00
if err != nil {
2021-12-16 20:01:14 +01:00
log . Error ( "Cannot open git repository %-v for issue #%d[%d]. Error: %v" , issue . Repo , issue . Index , issue . ID , err )
continue
2021-04-15 19:34:43 +02:00
}
gitRepos [ issue . RepoID ] = gitRepo
}
2022-04-27 00:40:01 +02:00
statuses , lastStatus , err := getAllCommitStatus ( gitRepo , issue . PullRequest )
2021-04-15 19:34:43 +02:00
if err != nil {
2022-04-27 00:40:01 +02:00
log . Error ( "getAllCommitStatus: cant get commit statuses of pull [%d]: %v" , issue . PullRequest . ID , err )
2021-05-04 14:03:02 +02:00
continue
2021-04-15 19:34:43 +02:00
}
2022-04-27 00:40:01 +02:00
res [ issue . PullRequest . ID ] = statuses
lastRes [ issue . PullRequest . ID ] = lastStatus
2021-04-15 19:34:43 +02:00
}
2022-04-27 00:40:01 +02:00
return res , lastRes , nil
2021-04-15 19:34:43 +02:00
}
2022-04-27 00:40:01 +02:00
// getAllCommitStatus get pr's commit statuses.
2022-06-13 11:37:59 +02:00
func getAllCommitStatus ( gitRepo * git . Repository , pr * issues_model . PullRequest ) ( statuses [ ] * git_model . CommitStatus , lastStatus * git_model . CommitStatus , err error ) {
2022-04-27 00:40:01 +02:00
sha , shaErr := gitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if shaErr != nil {
return nil , nil , shaErr
2020-04-10 13:26:37 +02:00
}
2022-06-12 17:51:54 +02:00
statuses , _ , err = git_model . GetLatestCommitStatus ( db . DefaultContext , pr . BaseRepo . ID , sha , db . ListOptions { } )
lastStatus = git_model . CalcCommitStatus ( statuses )
2022-04-27 00:40:01 +02:00
return statuses , lastStatus , err
2020-04-10 13:26:37 +02:00
}
// IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
2022-06-13 11:37:59 +02:00
func IsHeadEqualWithBranch ( ctx context . Context , pr * issues_model . PullRequest , branchName string ) ( bool , error ) {
2020-04-10 13:26:37 +02:00
var err error
2022-11-19 09:12:33 +01:00
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-04-10 13:26:37 +02:00
return false , err
}
2022-01-20 00:26:57 +01:00
baseGitRepo , closer , err := git . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo . RepoPath ( ) )
2020-04-10 13:26:37 +02:00
if err != nil {
return false , err
}
2022-01-20 00:26:57 +01:00
defer closer . Close ( )
2021-01-06 20:23:57 +01:00
2020-04-10 13:26:37 +02:00
baseCommit , err := baseGitRepo . GetBranchCommit ( branchName )
if err != nil {
return false , err
}
2022-11-19 09:12:33 +01:00
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-04-10 13:26:37 +02:00
return false , err
}
2022-01-20 00:26:57 +01:00
var headGitRepo * git . Repository
if pr . HeadRepoID == pr . BaseRepoID {
headGitRepo = baseGitRepo
} else {
var closer io . Closer
headGitRepo , closer , err = git . RepositoryFromContextOrOpen ( ctx , pr . HeadRepo . RepoPath ( ) )
if err != nil {
return false , err
}
defer closer . Close ( )
2020-04-10 13:26:37 +02:00
}
2021-01-06 20:23:57 +01:00
2021-07-28 11:42:56 +02:00
var headCommit * git . Commit
2022-06-13 11:37:59 +02:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2021-07-28 11:42:56 +02:00
headCommit , err = headGitRepo . GetBranchCommit ( pr . HeadBranch )
if err != nil {
return false , err
}
} else {
pr . HeadCommitID , err = baseGitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if err != nil {
return false , err
}
if headCommit , err = baseGitRepo . GetCommit ( pr . HeadCommitID ) ; err != nil {
return false , err
}
2020-04-10 13:26:37 +02:00
}
return baseCommit . HasPreviousCommit ( headCommit . ID )
}