2019-09-27 08:22:36 +08:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-09-27 08:22:36 +08:00
package pull
import (
2019-12-15 09:51:28 +00:00
"context"
2025-01-09 11:51:03 -08:00
"errors"
2019-09-27 08:22:36 +08:00
"fmt"
2022-01-19 23:26:57 +00:00
"io"
2021-06-25 19:01:43 +02:00
"regexp"
2020-01-09 02:47:45 +01:00
"strings"
2023-07-28 21:18:12 +02:00
"time"
2025-11-22 10:20:45 -07:00
"unicode/utf8"
2019-09-27 08:22:36 +08:00
2021-09-19 19:49:59 +08: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"
2024-11-09 12:48:31 +08:00
"code.gitea.io/gitea/models/organization"
2024-07-29 11:21:22 +09:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2024-07-29 11:21:22 +09:00
"code.gitea.io/gitea/models/unit"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2023-07-28 21:18:12 +02:00
"code.gitea.io/gitea/modules/base"
2022-10-12 07:18:26 +02:00
"code.gitea.io/gitea/modules/container"
2019-10-15 11:28:40 +08:00
"code.gitea.io/gitea/modules/git"
2025-09-15 23:33:12 -07:00
"code.gitea.io/gitea/modules/git/gitcmd"
2024-01-28 04:09:51 +08:00
"code.gitea.io/gitea/modules/gitrepo"
2024-09-06 18:12:41 +08:00
"code.gitea.io/gitea/modules/globallock"
2019-12-15 09:51:28 +00:00
"code.gitea.io/gitea/modules/graceful"
2019-09-27 08:22:36 +08:00
"code.gitea.io/gitea/modules/log"
2022-05-09 00:46:32 +08:00
repo_module "code.gitea.io/gitea/modules/repository"
2020-04-10 13:26:37 +02:00
"code.gitea.io/gitea/modules/setting"
2023-01-28 15:54:40 +00:00
"code.gitea.io/gitea/modules/util"
2025-12-25 17:51:30 -08:00
git_service "code.gitea.io/gitea/services/git"
2019-10-29 00:45:43 +08: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-09-27 08:22:36 +08:00
)
2024-09-06 18:12:41 +08:00
func getPullWorkingLockKey ( prID int64 ) string {
return fmt . Sprintf ( "pull_working_%d" , prID )
}
2022-05-04 18:06:23 +02:00
2024-11-09 12:48:31 +08:00
type NewPullRequestOptions struct {
Repo * repo_model . Repository
Issue * issues_model . Issue
LabelIDs [ ] int64
AttachmentUUIDs [ ] string
PullRequest * issues_model . PullRequest
AssigneeIDs [ ] int64
Reviewers [ ] * user_model . User
TeamReviewers [ ] * organization . Team
2026-01-04 08:45:36 -08:00
ProjectID int64
2024-11-09 12:48:31 +08:00
}
2019-09-27 08:22:36 +08:00
// NewPullRequest creates new pull request with labels for repository.
2024-11-09 12:48:31 +08:00
func NewPullRequest ( ctx context . Context , opts * NewPullRequestOptions ) error {
repo , issue , labelIDs , uuids , pr , assigneeIDs := opts . Repo , opts . Issue , opts . LabelIDs , opts . AttachmentUUIDs , opts . PullRequest , opts . AssigneeIDs
2024-03-04 09:16:03 +01:00
if err := issue . LoadPoster ( ctx ) ; err != nil {
return err
}
if user_model . IsUserBlockedBy ( ctx , issue . Poster , repo . OwnerID ) || user_model . IsUserBlockedBy ( ctx , issue . Poster , assigneeIDs ... ) {
return user_model . ErrBlockedUser
}
2024-07-29 11:21:22 +09:00
// user should be a collaborator or a member of the organization for base repo
2024-12-28 02:17:01 +08:00
canCreate := issue . Poster . IsAdmin || pr . Flow == issues_model . PullRequestFlowAGit
2026-01-04 08:45:36 -08:00
canAssignProject := canCreate
2024-12-28 02:17:01 +08:00
if ! canCreate {
2024-07-29 11:21:22 +09:00
canCreate , err := repo_model . IsOwnerMemberCollaborator ( ctx , repo , issue . Poster . ID )
if err != nil {
return err
}
2026-01-04 08:45:36 -08:00
canAssignProject = canCreate
2024-07-29 11:21:22 +09:00
if ! canCreate {
// or user should have write permission in the head repo
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
return err
}
2026-03-29 11:21:14 +02:00
perm , err := access_model . GetDoerRepoPermission ( ctx , pr . HeadRepo , issue . Poster )
2024-07-29 11:21:22 +09:00
if err != nil {
return err
}
if ! perm . CanWrite ( unit . TypeCode ) {
return issues_model . ErrMustCollaborator
}
2026-01-04 08:45:36 -08:00
canAssignProject = perm . CanWrite ( unit . TypeProjects )
2024-07-29 11:21:22 +09:00
}
}
2026-01-27 11:57:20 -08:00
if err := checkPullRequestBranchMergeable ( ctx , pr ) ; err != nil {
2019-12-15 11:28:51 +08:00
return err
}
2023-08-10 10:39:21 +08:00
assigneeCommentMap := make ( map [ int64 ] * issues_model . Comment )
2019-09-27 08:22:36 +08:00
2024-04-27 10:03:49 +02:00
var reviewNotifiers [ ] * issue_service . ReviewRequestNotifier
2023-08-10 10:39:21 +08:00
if err := db . WithTx ( ctx , func ( ctx context . Context ) error {
if err := issues_model . NewPullRequest ( ctx , repo , issue , labelIDs , uuids , pr ) ; err != nil {
return err
}
for _ , assigneeID := range assigneeIDs {
comment , err := issue_service . AddAssigneeIfNotAssigned ( ctx , issue , issue . Poster , assigneeID , false )
if err != nil {
return err
}
assigneeCommentMap [ assigneeID ] = comment
}
2026-01-04 08:45:36 -08:00
if opts . ProjectID > 0 && canAssignProject {
if err := issues_model . IssueAssignOrRemoveProject ( ctx , issue , issue . Poster , opts . ProjectID , 0 ) ; err != nil {
return err
}
}
2023-08-10 10:39:21 +08:00
pr . Issue = issue
issue . PullRequest = pr
2026-01-27 11:57:20 -08:00
var err error
2023-08-10 10:39:21 +08:00
if pr . Flow == issues_model . PullRequestFlowGithub {
err = PushToBaseRepo ( ctx , pr )
} else {
err = UpdateRef ( ctx , pr )
}
if err != nil {
return err
}
2025-09-26 08:15:42 -07:00
// Update Commit Divergence
err = syncCommitDivergence ( ctx , pr )
if err != nil {
return err
}
2025-09-19 08:04:18 -07:00
// add first push codes comment
2026-04-04 16:27:57 -07:00
if _ , _ , err := CreatePushPullComment ( ctx , issue . Poster , pr , git . BranchPrefix + pr . BaseBranch , pr . GetGitHeadRefName ( ) , false ) ; err != nil {
2023-08-10 10:39:21 +08:00
return err
}
2023-06-08 11:56:05 +03:00
2023-10-11 06:24:07 +02:00
if ! pr . IsWorkInProgress ( ctx ) {
2025-03-13 19:36:14 -07:00
reviewNotifiers , err = issue_service . PullRequestCodeOwnersReview ( ctx , pr )
2024-03-19 06:28:43 +01:00
if err != nil {
2023-06-08 11:56:05 +03:00
return err
}
}
2023-08-10 10:39:21 +08:00
return nil
} ) ; err != nil {
// cleanup: this will only remove the reference, the real commit will be clean up when next GC
2025-09-19 08:04:18 -07:00
if err1 := gitrepo . RemoveRef ( ctx , pr . BaseRepo , pr . GetGitHeadRefName ( ) ) ; err1 != nil {
log . Error ( "RemoveRef: %v" , err1 )
2023-08-10 10:39:21 +08:00
}
return err
}
2023-06-08 11:56:05 +03:00
2024-04-27 10:03:49 +02:00
issue_service . ReviewRequestNotify ( ctx , issue , issue . Poster , reviewNotifiers )
2024-03-19 06:28:43 +01:00
2025-10-23 20:08:21 -07:00
// Request reviews, these should be requested before other notifications because they will add request reviews record
// on database
2026-03-29 11:21:14 +02:00
permDoer , err := access_model . GetDoerRepoPermission ( ctx , repo , issue . Poster )
2026-01-27 11:57:20 -08:00
if err != nil {
return err
}
2025-10-23 20:08:21 -07:00
for _ , reviewer := range opts . Reviewers {
if _ , err = issue_service . ReviewRequest ( ctx , pr . Issue , issue . Poster , & permDoer , reviewer , true ) ; err != nil {
return err
}
}
for _ , teamReviewer := range opts . TeamReviewers {
if _ , err = issue_service . TeamReviewRequest ( ctx , pr . Issue , issue . Poster , teamReviewer , true ) ; err != nil {
return err
}
}
2023-08-10 10:39:21 +08:00
mentions , err := issues_model . FindAndUpdateIssueMentions ( ctx , issue , issue . Poster , issue . Content )
if err != nil {
return err
}
2023-09-06 02:37:47 +08:00
notify_service . NewPullRequest ( ctx , pr , mentions )
2023-08-10 10:39:21 +08:00
if len ( issue . Labels ) > 0 {
2023-09-06 02:37:47 +08:00
notify_service . IssueChangeLabels ( ctx , issue . Poster , issue , issue . Labels , nil )
2023-08-10 10:39:21 +08:00
}
if issue . Milestone != nil {
2023-09-06 02:37:47 +08:00
notify_service . IssueChangeMilestone ( ctx , issue . Poster , issue , 0 )
2023-08-10 10:39:21 +08:00
}
2023-10-06 14:49:37 +08:00
for _ , assigneeID := range assigneeIDs {
assignee , err := user_model . GetUserByID ( ctx , assigneeID )
if err != nil {
return ErrDependenciesLeft
2023-08-10 10:39:21 +08:00
}
2023-10-06 14:49:37 +08:00
notify_service . IssueChangeAssignee ( ctx , issue . Poster , issue , assignee , false , assigneeCommentMap [ assigneeID ] )
2020-05-20 20:47:24 +08:00
}
2025-10-23 20:08:21 -07:00
2019-09-27 08:22:36 +08:00
return nil
}
2019-10-15 11:28:40 +08:00
2024-12-20 10:05:29 -08:00
// ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error
type ErrPullRequestHasMerged struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged.
func IsErrPullRequestHasMerged ( err error ) bool {
_ , ok := err . ( ErrPullRequestHasMerged )
return ok
}
// Error does pretty-printing :D
func ( err ErrPullRequestHasMerged ) Error ( ) string {
return fmt . Sprintf ( "pull request has merged [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]" ,
err . ID , err . IssueID , err . HeadRepoID , err . BaseRepoID , err . HeadBranch , err . BaseBranch )
}
2019-12-16 07:20:25 +01:00
// ChangeTargetBranch changes the target branch of this pull request, as the given user.
2022-06-13 17:37:59 +08:00
func ChangeTargetBranch ( ctx context . Context , pr * issues_model . PullRequest , doer * user_model . User , targetBranch string ) ( err 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
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 17:37:59 +08: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 ,
2025-01-07 19:16:56 -08:00
IsPull : true ,
2019-12-16 07:20:25 +01:00
}
}
if pr . HasMerged {
2024-12-20 10:05:29 -08:00
return ErrPullRequestHasMerged {
2019-12-16 07:20:25 +01:00
ID : pr . ID ,
IssueID : pr . Index ,
HeadRepoID : pr . HeadRepoID ,
BaseRepoID : pr . BaseRepoID ,
HeadBranch : pr . HeadBranch ,
BaseBranch : pr . BaseBranch ,
}
}
2025-10-03 15:16:17 -07:00
exist , err := git_model . IsBranchExist ( ctx , pr . BaseRepoID , targetBranch )
if err != nil {
return err
}
if ! exist {
return git_model . ErrBranchNotExist {
RepoID : pr . BaseRepoID ,
BranchName : targetBranch ,
}
}
2019-12-16 07:20:25 +01:00
// Check if branches are equal
2022-01-19 23:26:57 +00: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 18:03:20 +08: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 17:37:59 +08: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 17:37:59 +08: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
2026-01-27 11:57:20 -08:00
if err := checkPullRequestBranchMergeable ( ctx , pr ) ; err != nil {
2019-12-16 07:20:25 +01:00
return err
}
// Update target branch, PR diff and status
2025-04-24 21:26:57 +02:00
// This is the same as markPullRequestAsMergeable in check service, but also updates base_branch
2022-06-13 17:37:59 +08: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
2025-09-09 12:40:54 -07:00
// add first push codes comment
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2025-09-26 08:15:42 -07:00
// The UPDATE acquires the transaction lock, if the UPDATE succeeds, it should have updated one row (the "base_branch" is changed)
// If no row is updated, it means the PR has been merged or closed in the meantime
updated , err := pr . UpdateColsIfNotMerged ( ctx , "merge_base" , "status" , "conflicted_files" , "changed_protected_files" , "base_branch" )
if err != nil {
2025-09-09 12:40:54 -07:00
return err
}
2025-09-26 08:15:42 -07:00
if updated == 0 {
return util . ErrorWrap ( util . ErrInvalidArgument , "pull request status has changed" )
}
if err := syncCommitDivergence ( ctx , pr ) ; err != nil {
return fmt . Errorf ( "syncCommitDivergence: %w" , err )
}
2019-12-16 07:20:25 +01:00
2025-09-09 12:40:54 -07:00
// Create comment
options := & issues_model . CreateCommentOptions {
Type : issues_model . CommentTypeChangeTargetBranch ,
Doer : doer ,
Repo : pr . Issue . Repo ,
Issue : pr . Issue ,
OldRef : oldBranch ,
NewRef : targetBranch ,
}
if _ , err = issues_model . CreateComment ( ctx , options ) ; err != nil {
return fmt . Errorf ( "CreateChangeTargetBranchComment: %w" , err )
}
// Delete all old push comments and insert new push comments
if _ , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , issues_model . CommentTypePullRequestPush ) .
NoAutoCondition ( ) .
Delete ( new ( issues_model . Comment ) ) ; err != nil {
return err
}
2026-04-04 16:27:57 -07:00
_ , _ , err = CreatePushPullComment ( ctx , doer , pr , git . BranchPrefix + pr . BaseBranch , pr . GetGitHeadRefName ( ) , false )
2025-09-09 12:40:54 -07:00
return err
} )
2019-12-16 07:20:25 +01:00
}
2022-06-13 17:37:59 +08:00
func checkForInvalidation ( ctx context . Context , requests issues_model . PullRequestList , repoID int64 , doer * user_model . User , branch string ) error {
2022-12-03 10:48:26 +08:00
repo , err := repo_model . GetRepositoryByID ( ctx , repoID )
2019-10-15 11:28:40 +08:00
if err != nil {
2022-11-19 09:12:33 +01:00
return fmt . Errorf ( "GetRepositoryByIDCtx: %w" , err )
2019-10-15 11:28:40 +08:00
}
2024-01-28 04:09:51 +08:00
gitRepo , err := gitrepo . OpenRepository ( ctx , repo )
2019-10-15 11:28:40 +08:00
if err != nil {
2024-01-28 04:09:51 +08:00
return fmt . Errorf ( "gitrepo.OpenRepository: %w" , err )
2019-10-15 11:28:40 +08:00
}
go func ( ) {
2019-12-15 09:51:28 +00:00
// FIXME: graceful: We need to tell the manager we're doing something...
2025-09-26 10:14:20 -07:00
err := InvalidateCodeComments ( ctx , requests , doer , repo , gitRepo , branch )
2019-10-15 11:28:40 +08:00
if err != nil {
log . Error ( "PullRequestList.InvalidateCodeComments: %v" , err )
}
2019-11-13 07:01:19 +00:00
gitRepo . Close ( )
2019-10-15 11:28:40 +08:00
} ( )
return nil
}
2025-03-13 19:36:14 -07:00
type TestPullRequestOptions struct {
RepoID int64
Doer * user_model . User
Branch string
IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
IsForcePush bool
OldCommitID string
NewCommitID string
}
2019-10-15 11:28:40 +08:00
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed.
2025-03-13 19:36:14 -07:00
func AddTestPullRequestTask ( opts TestPullRequestOptions ) {
log . Trace ( "AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests" , opts . RepoID , opts . Branch )
2019-12-15 09:51:28 +00:00
graceful . GetManager ( ) . RunWithShutdownContext ( func ( ctx context . Context ) {
2025-10-14 12:19:27 -07:00
// this function does a lot of operations to various models, if the process gets killed in the middle,
// there is no way to recover at the moment. The best workaround is to let end user push again.
2025-09-26 08:15:42 -07:00
repo , err := repo_model . GetRepositoryByID ( ctx , opts . RepoID )
if err != nil {
log . Error ( "GetRepositoryByID: %v" , err )
return
}
2023-05-08 14:39:32 +08:00
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
2025-09-26 08:15:42 -07:00
headBranchPRs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( ctx , opts . RepoID , opts . Branch )
2019-12-15 09:51:28 +00:00
if err != nil {
2025-03-13 19:36:14 -07:00
log . Error ( "Find pull requests [head_repo_id: %d, head_branch: %s]: %v" , opts . RepoID , opts . Branch , err )
2019-12-15 09:51:28 +00:00
return
2019-10-15 11:28:40 +08:00
}
2019-12-15 09:51:28 +00:00
2025-09-26 08:15:42 -07:00
for _ , pr := range headBranchPRs {
2023-02-05 19:57:38 +08:00
log . Trace ( "Updating PR[%d]: composing new test task" , pr . ID )
2025-09-26 08:15:42 -07:00
pr . HeadRepo = repo // avoid loading again
2023-02-05 19:57:38 +08:00
if pr . Flow == issues_model . PullRequestFlowGithub {
if err := PushToBaseRepo ( ctx , pr ) ; err != nil {
log . Error ( "PushToBaseRepo: %v" , err )
continue
}
} else {
continue
}
2025-10-14 12:19:27 -07:00
// create push comment before check pull request status,
// then when the status is mergeable, the comment is already in database, to make testing easy and stable
2026-04-04 16:27:57 -07:00
comment , commentCreated , err := CreatePushPullComment ( ctx , opts . Doer , pr , opts . OldCommitID , opts . NewCommitID , opts . IsForcePush )
if err != nil {
log . Error ( "CreatePushPullComment: %v" , err )
} else if commentCreated {
2025-03-13 19:36:14 -07:00
notify_service . PullRequestPushCommits ( ctx , opts . Doer , pr , comment )
2023-02-05 19:57:38 +08:00
}
2025-10-14 12:19:27 -07:00
// The caller can be in a goroutine or a "push queue", "conflict check" can be time-consuming,
// and the concurrency should be limited, so the conflict check will be done in another queue
StartPullRequestCheckImmediately ( ctx , pr )
2023-02-05 19:57:38 +08:00
}
2025-03-13 19:36:14 -07:00
if opts . IsSync {
2025-09-26 08:15:42 -07:00
if err = headBranchPRs . LoadAttributes ( ctx ) ; err != nil {
2019-12-15 09:51:28 +00:00
log . Error ( "PullRequestList.LoadAttributes: %v" , err )
}
2025-09-26 08:15:42 -07:00
if invalidationErr := checkForInvalidation ( ctx , headBranchPRs , opts . RepoID , opts . Doer , opts . Branch ) ; invalidationErr != nil {
2019-12-15 09:51:28 +00:00
log . Error ( "checkForInvalidation: %v" , invalidationErr )
}
if err == nil {
2025-09-26 08:15:42 -07:00
for _ , pr := range headBranchPRs {
2024-03-11 05:30:36 +08:00
objectFormat := git . ObjectFormatFromName ( pr . BaseRepo . ObjectFormatName )
2025-03-13 19:36:14 -07:00
if opts . NewCommitID != "" && opts . NewCommitID != objectFormat . EmptyObjectID ( ) . String ( ) {
2025-11-28 12:33:52 -07:00
changed , newMergeBase , err := checkIfPRContentChanged ( ctx , pr , opts . OldCommitID , opts . NewCommitID )
2020-01-09 02:47:45 +01:00
if err != nil {
log . Error ( "checkIfPRContentChanged: %v" , err )
}
2025-11-28 12:33:52 -07:00
if newMergeBase != "" && pr . MergeBase != newMergeBase {
pr . MergeBase = newMergeBase
if _ , err := pr . UpdateColsIfNotMerged ( ctx , "merge_base" ) ; err != nil {
log . Error ( "Update merge base for %-v: %v" , pr , err )
}
}
2020-01-09 02:47:45 +01:00
if changed {
// Mark old reviews as stale if diff to mergebase has changed
2023-09-29 14:12:54 +02:00
if err := issues_model . MarkReviewsAsStale ( ctx , pr . IssueID ) ; err != nil {
2020-01-09 02:47:45 +01:00
log . Error ( "MarkReviewsAsStale: %v" , err )
}
2023-07-20 15:18:52 +08: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 {
2025-03-13 19:36:14 -07:00
if err := DismissApprovalReviews ( ctx , opts . Doer , pr ) ; err != nil {
2023-07-20 15:18:52 +08:00
log . Error ( "DismissApprovalReviews: %v" , err )
}
}
2020-01-09 02:47:45 +01:00
}
2025-03-13 19:36:14 -07:00
if err := issues_model . MarkReviewsAsNotStale ( ctx , pr . IssueID , opts . NewCommitID ) ; err != nil {
2020-01-09 02:47:45 +01:00
log . Error ( "MarkReviewsAsNotStale: %v" , err )
}
2025-09-26 08:15:42 -07:00
if err = syncCommitDivergence ( ctx , pr ) ; err != nil {
log . Error ( "syncCommitDivergence: %v" , err )
2020-04-14 15:53:34 +02:00
}
2020-01-09 02:47:45 +01:00
}
2025-03-13 19:36:14 -07:00
if ! pr . IsWorkInProgress ( ctx ) {
2025-04-28 15:57:56 -07:00
reviewNotifiers , err := issue_service . PullRequestCodeOwnersReview ( ctx , pr )
2025-03-13 19:36:14 -07:00
if err != nil {
log . Error ( "PullRequestCodeOwnersReview: %v" , err )
}
if len ( reviewNotifiers ) > 0 {
issue_service . ReviewRequestNotify ( ctx , pr . Issue , opts . Doer , reviewNotifiers )
}
}
notify_service . PullRequestSynchronized ( ctx , opts . Doer , pr )
2019-12-15 09:51:28 +00:00
}
2019-10-15 11:28:40 +08:00
}
}
2025-03-13 19:36:14 -07:00
log . Trace ( "AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests" , opts . RepoID , opts . Branch )
2025-09-26 08:15:42 -07:00
// The base repositories of baseBranchPRs are the same one (opts.RepoID)
baseBranchPRs , err := issues_model . GetUnmergedPullRequestsByBaseInfo ( ctx , opts . RepoID , opts . Branch )
2019-12-15 09:51:28 +00:00
if err != nil {
2025-03-13 19:36:14 -07:00
log . Error ( "Find pull requests [base_repo_id: %d, base_branch: %s]: %v" , opts . RepoID , opts . Branch , err )
2019-12-15 09:51:28 +00:00
return
}
2025-09-26 08:15:42 -07:00
for _ , pr := range baseBranchPRs {
pr . BaseRepo = repo // avoid loading again
err = syncCommitDivergence ( ctx , pr )
2020-04-14 15:53:34 +02:00
if err != nil {
2025-09-26 08:15:42 -07:00
if errors . Is ( err , util . ErrNotExist ) {
log . Warn ( "Cannot test PR %s/%d with base=%s head=%s: no longer exists" , pr . BaseRepo . FullName ( ) , pr . IssueID , pr . BaseBranch , pr . HeadBranch )
2021-07-13 01:26:25 +02:00
} else {
2025-09-26 08:15:42 -07:00
log . Error ( "syncCommitDivergence: %v" , err )
2020-04-14 15:53:34 +02:00
}
2025-09-26 08:15:42 -07:00
continue
2020-04-14 15:53:34 +02:00
}
2025-04-24 21:26:57 +02:00
StartPullRequestCheckDelayable ( ctx , pr )
2019-12-15 09:51:28 +00:00
}
} )
2019-10-15 11:28:40 +08:00
}
2019-12-15 11:28:51 +08: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
2025-11-28 12:33:52 -07:00
func checkIfPRContentChanged ( ctx context . Context , pr * issues_model . PullRequest , oldCommitID , newCommitID string ) ( hasChanged bool , mergeBase string , err error ) {
2025-09-26 08:15:42 -07:00
prCtx , cancel , err := createTemporaryRepoForPR ( ctx , pr ) // FIXME: why it still needs to create a temp repo, since the alongside calls like GetDiverging doesn't do so anymore
2020-01-09 02:47:45 +01:00
if err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "CreateTemporaryRepoForPR %-v: %v" , pr , err )
2025-11-28 12:33:52 -07:00
return false , "" , err
2020-01-09 02:47:45 +01:00
}
2023-03-07 20:07:35 +00:00
defer cancel ( )
2023-01-28 15:54:40 +00:00
2026-01-17 11:22:09 -08:00
mergeBase , err = gitrepo . MergeBase ( ctx , pr . BaseRepo , pr . BaseBranch , pr . GetGitHeadRefName ( ) )
2023-01-28 15:54:40 +00:00
if err != nil {
2025-11-28 12:33:52 -07:00
return false , "" , fmt . Errorf ( "GetMergeBase: %w" , err )
2020-01-09 02:47:45 +01:00
}
2025-11-28 12:33:52 -07:00
cmd := gitcmd . NewCommand ( "diff" , "--name-only" , "-z" ) . AddDynamicArguments ( newCommitID , oldCommitID , mergeBase )
2023-01-28 15:54:40 +00:00
2026-01-22 14:04:26 +08:00
stdoutReader , stdoutReaderClose := cmd . MakeStdoutPipe ( )
defer stdoutReaderClose ( )
2025-10-07 02:06:51 -07:00
if err := cmd . WithDir ( prCtx . tmpBasePath ) .
2026-01-21 09:35:14 +08:00
WithPipelineFunc ( func ( ctx gitcmd . Context ) error {
2023-01-28 15:54:40 +00:00
return util . IsEmptyReader ( stdoutReader )
2025-10-07 02:06:51 -07:00
} ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( ctx ) ; err != nil {
if errors . Is ( err , util . ErrNotEmpty ) {
2025-11-28 12:33:52 -07:00
return true , mergeBase , nil
2020-01-09 02:47:45 +01:00
}
2023-01-28 15:54:40 +00:00
log . Error ( "Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v" ,
2025-11-28 12:33:52 -07:00
newCommitID , oldCommitID , mergeBase ,
2023-01-28 15:54:40 +00:00
pr . ID , pr . BaseRepo . FullName ( ) , pr . BaseBranch , pr . HeadRepo . FullName ( ) , pr . HeadBranch ,
err )
2025-11-28 12:33:52 -07:00
return false , mergeBase , fmt . Errorf ( "Unable to run git diff --name-only -z %s %s %s: %w" , newCommitID , oldCommitID , mergeBase , err )
2020-01-09 02:47:45 +01:00
}
2025-11-28 12:33:52 -07:00
return false , mergeBase , nil
2020-01-09 02:47:45 +01:00
}
2019-12-15 11:28:51 +08: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 17:37:59 +08:00
func PushToBaseRepo ( ctx context . Context , pr * issues_model . PullRequest ) ( err error ) {
2022-01-19 23:26:57 +00:00
return pushToBaseRepoHelper ( ctx , pr , "" )
2021-06-24 00:08:26 +03:00
}
2022-06-13 17:37:59 +08:00
func pushToBaseRepoHelper ( ctx context . Context , pr * issues_model . PullRequest , prefixHeadBranch string ) ( err error ) {
2025-07-16 21:33:33 +08:00
log . Trace ( "PushToBaseRepo[%d]: pushing commits to base repo '%s'" , pr . BaseRepoID , pr . GetGitHeadRefName ( ) )
2019-12-15 11:28:51 +08:00
2022-11-19 09:12:33 +01:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-02-21 18:18:13 +00:00
log . Error ( "Unable to load head repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2020-01-28 10:23:58 +00:00
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-02-21 18:18:13 +00:00
log . Error ( "Unable to load base repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2019-12-15 11:28:51 +08: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 21:15:04 +00: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 21:15:04 +00:00
}
2025-07-16 21:33:33 +08:00
gitRefName := pr . GetGitHeadRefName ( )
2020-09-15 04:32:31 +01:00
2025-12-10 09:41:01 -08:00
if err := gitrepo . Push ( ctx , pr . HeadRepo , pr . BaseRepo , git . PushOptions {
2021-06-24 00:08:26 +03:00
Branch : prefixHeadBranch + pr . HeadBranch + ":" + gitRefName ,
2019-12-15 11:28:51 +08:00
Force : true ,
2019-12-27 21:15:04 +00:00
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
2022-05-09 00:46:32 +08:00
Env : repo_module . InternalPushingEnvironment ( pr . Issue . Poster , pr . BaseRepo ) ,
2019-12-15 11:28:51 +08:00
} ) ; err != nil {
2020-06-08 19:07:41 +01:00
if git . IsErrPushOutOfDate ( err ) {
// This should not happen as we're using force!
2020-09-15 04:32:31 +01: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 19:07:41 +01:00
return err
} else if git . IsErrPushRejected ( err ) {
rejectErr := err . ( * git . ErrPushRejected )
2020-09-15 04:32:31 +01: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 19:07:41 +01:00
return err
2021-06-24 00:08:26 +03:00
} else if git . IsErrMoreThanOne ( err ) {
if prefixHeadBranch != "" {
log . Info ( "Can't push with %s%s" , prefixHeadBranch , pr . HeadBranch )
return err
}
2022-01-19 23:26:57 +00:00
log . Info ( "Retrying to push with %s%s" , git . BranchPrefix , pr . HeadBranch )
err = pushToBaseRepoHelper ( ctx , pr , git . BranchPrefix )
2021-06-24 00:08:26 +03:00
return err
2020-06-08 19:07:41 +01:00
}
2020-09-15 04:32:31 +01: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 11:28:51 +08:00
}
return nil
}
2020-01-25 10:48:22 +08:00
2024-03-27 10:34:10 +08:00
// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
func UpdatePullsRefs ( ctx context . Context , repo * repo_model . Repository , update * repo_module . PushUpdateOptions ) {
branch := update . RefFullName . BranchName ( )
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( ctx , repo . ID , branch )
if err != nil {
log . Error ( "Find pull requests [head_repo_id: %d, head_branch: %s]: %v" , repo . ID , branch , err )
} else {
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 )
}
}
}
}
}
2021-07-28 17:42:56 +08:00
// UpdateRef update refs/pull/id/head directly for agit flow pull request
2022-06-13 17:37:59 +08:00
func UpdateRef ( ctx context . Context , pr * issues_model . PullRequest ) ( err error ) {
2025-07-16 21:33:33 +08:00
log . Trace ( "UpdateRef[%d]: upgate pull request ref in base repo '%s'" , pr . ID , pr . GetGitHeadRefName ( ) )
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2021-07-28 17:42:56 +08:00
log . Error ( "Unable to load base repository for PR[%d] Error: %v" , pr . ID , err )
return err
}
2025-09-19 08:04:18 -07:00
if err := gitrepo . UpdateRef ( ctx , pr . BaseRepo , pr . GetGitHeadRefName ( ) , pr . HeadCommitID ) ; err != nil {
2021-07-28 17:42:56 +08:00
log . Error ( "Unable to update ref in base repository for PR[%d] Error: %v" , pr . ID , err )
}
return err
}
2025-01-09 11:51:03 -08:00
// retargetBranchPulls change target branch for all pull requests whose base branch is the branch
2024-01-17 03:44:56 +03:00
// Both branch and targetBranch must be in the same repo (for security reasons)
2025-01-09 11:51:03 -08:00
func retargetBranchPulls ( ctx context . Context , doer * user_model . User , repoID int64 , branch , targetBranch string ) error {
2024-01-17 03:44:56 +03:00
prs , err := issues_model . GetUnmergedPullRequestsByBaseInfo ( ctx , repoID , branch )
if err != nil {
return err
}
2025-03-02 10:14:49 -08:00
if err := prs . LoadAttributes ( ctx ) ; err != nil {
2024-01-17 03:44:56 +03:00
return err
}
2025-01-09 11:51:03 -08:00
var errs [ ] error
2024-01-17 03:44:56 +03:00
for _ , pr := range prs {
if err = pr . Issue . LoadRepo ( ctx ) ; err != nil {
errs = append ( errs , err )
} else if err = ChangeTargetBranch ( ctx , pr , doer , targetBranch ) ; err != nil &&
2024-12-20 10:05:29 -08:00
! issues_model . IsErrIssueIsClosed ( err ) && ! IsErrPullRequestHasMerged ( err ) &&
2024-01-17 03:44:56 +03:00
! issues_model . IsErrPullRequestAlreadyExists ( err ) {
errs = append ( errs , err )
}
}
2025-01-09 11:51:03 -08:00
return errors . Join ( errs ... )
2024-01-17 03:44:56 +03:00
}
2025-01-09 11:51:03 -08:00
// AdjustPullsCausedByBranchDeleted close all the pull requests who's head branch is the branch
// Or Close all the plls who's base branch is the branch if setting.Repository.PullRequest.RetargetChildrenOnMerge is false.
// If it's true, Retarget all these pulls to the default branch.
func AdjustPullsCausedByBranchDeleted ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , branch string ) error {
// branch as head branch
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( ctx , repo . ID , branch )
2020-01-25 10:48:22 +08:00
if err != nil {
return err
}
2025-03-02 10:14:49 -08:00
if err := prs . LoadAttributes ( ctx ) ; err != nil {
2025-01-09 11:51:03 -08:00
return err
}
2025-03-02 10:14:49 -08:00
prs . SetHeadRepo ( repo )
if err := prs . LoadRepositories ( ctx ) ; err != nil {
2025-01-09 11:51:03 -08:00
return err
}
var errs [ ] error
for _ , pr := range prs {
if err = issue_service . CloseIssue ( ctx , pr . Issue , doer , "" ) ; err != nil && ! issues_model . IsErrIssueIsClosed ( err ) && ! issues_model . IsErrDependenciesLeft ( err ) {
errs = append ( errs , err )
}
if err == nil {
if err := issues_model . AddDeletePRBranchComment ( ctx , doer , pr . BaseRepo , pr . Issue . ID , pr . HeadBranch ) ; err != nil {
log . Error ( "AddDeletePRBranchComment: %v" , err )
errs = append ( errs , err )
}
}
}
if setting . Repository . PullRequest . RetargetChildrenOnMerge {
if err := retargetBranchPulls ( ctx , doer , repo . ID , branch , repo . DefaultBranch ) ; err != nil {
log . Error ( "retargetBranchPulls failed: %v" , err )
errs = append ( errs , err )
}
return errors . Join ( errs ... )
}
// branch as base branch
prs , err = issues_model . GetUnmergedPullRequestsByBaseInfo ( ctx , repo . ID , branch )
2020-01-25 10:48:22 +08:00
if err != nil {
return err
}
2025-03-02 10:14:49 -08:00
if err := prs . LoadAttributes ( ctx ) ; err != nil {
2020-01-25 10:48:22 +08:00
return err
}
2025-03-02 10:14:49 -08:00
prs . SetBaseRepo ( repo )
if err := prs . LoadRepositories ( ctx ) ; err != nil {
2025-01-09 11:51:03 -08:00
return err
}
2020-01-25 10:48:22 +08:00
2025-01-09 11:51:03 -08:00
errs = nil
2020-01-25 10:48:22 +08:00
for _ , pr := range prs {
2025-01-09 11:51:03 -08:00
if err = issues_model . AddDeletePRBranchComment ( ctx , doer , pr . BaseRepo , pr . Issue . ID , pr . BaseBranch ) ; err != nil {
log . Error ( "AddDeletePRBranchComment: %v" , err )
2020-01-25 10:48:22 +08:00
errs = append ( errs , err )
}
2025-01-09 11:51:03 -08:00
if err == nil {
if err = issue_service . CloseIssue ( ctx , pr . Issue , doer , "" ) ; err != nil && ! issues_model . IsErrIssueIsClosed ( err ) && ! issues_model . IsErrDependenciesLeft ( err ) {
errs = append ( errs , err )
}
}
2020-01-25 10:48:22 +08:00
}
2025-01-09 11:51:03 -08:00
return errors . Join ( errs ... )
2020-01-25 10:48:22 +08:00
}
2021-03-01 17:39:44 +00: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-19 23:26:57 +00:00
func CloseRepoBranchesPulls ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository ) error {
2024-01-28 04:09:51 +08:00
branches , _ , err := gitrepo . GetBranchesByPath ( ctx , repo , 0 , 0 )
2020-01-25 10:48:22 +08:00
if err != nil {
return err
}
2025-01-09 11:51:03 -08:00
var errs [ ] error
2020-01-25 10:48:22 +08:00
for _ , branch := range branches {
2025-04-02 10:31:32 -07:00
prs , err := issues_model . GetUnmergedPullRequestsByHeadInfo ( ctx , repo . ID , branch )
2020-01-25 10:48:22 +08:00
if err != nil {
return err
}
2025-03-02 10:14:49 -08:00
if err = prs . LoadAttributes ( ctx ) ; err != nil {
2020-01-25 10:48:22 +08:00
return err
}
for _ , pr := range prs {
2021-03-01 17:39:44 +00: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
}
2025-01-07 19:16:56 -08:00
if err = issue_service . CloseIssue ( ctx , pr . Issue , doer , "" ) ; err != nil && ! issues_model . IsErrIssueIsClosed ( err ) {
2020-01-25 10:48:22 +08:00
errs = append ( errs , err )
}
}
}
2025-01-09 11:51:03 -08:00
return errors . Join ( errs ... )
2020-01-25 10:48:22 +08:00
}
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-22 00:46:14 +08:00
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
2022-06-13 17:37:59 +08: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 10:48:26 +08: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 ""
}
}
2024-01-28 04:09:51 +08:00
gitRepo , closer , err := gitrepo . RepositoryFromContextOrOpen ( ctx , pr . HeadRepo )
2020-04-10 13:26:37 +02:00
if err != nil {
log . Error ( "Unable to open head repository: Error: %v" , err )
return ""
}
2022-01-19 23:26:57 +00:00
defer closer . Close ( )
2020-04-10 13:26:37 +02:00
2021-07-28 17:42:56 +08:00
var headCommit * git . Commit
2022-06-13 17:37:59 +08:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2021-07-28 17:42:56 +08:00
headCommit , err = gitRepo . GetBranchCommit ( pr . HeadBranch )
} else {
2025-07-16 21:33:33 +08:00
pr . HeadCommitID , err = gitRepo . GetRefCommitID ( pr . GetGitHeadRefName ( ) )
2021-07-28 17:42:56 +08:00
if err != nil {
2025-07-16 21:33:33 +08:00
log . Error ( "Unable to get head commit: %s Error: %v" , pr . GetGitHeadRefName ( ) , err )
2021-07-28 17:42:56 +08:00
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-22 00:46:14 +08:00
2021-06-18 17:08:22 -05:00
if ! setting . Repository . PullRequest . PopulateSquashCommentWithCommitMessages {
2025-11-22 10:20:45 -07:00
// use PR's title and description as squash commit message
2021-06-25 19:01:43 +02:00
message := strings . TrimSpace ( pr . Issue . Content )
stringBuilder . WriteString ( message )
2021-06-18 17:08:22 -05:00
if stringBuilder . Len ( ) > 0 {
stringBuilder . WriteRune ( '\n' )
2021-06-25 19:01:43 +02:00
if ! commitMessageTrailersPattern . MatchString ( message ) {
2025-11-22 10:20:45 -07:00
// TODO: this trailer check doesn't work with the separator line added below for the co-authors
2021-06-25 19:01:43 +02:00
stringBuilder . WriteRune ( '\n' )
}
2021-06-18 17:08:22 -05:00
}
2025-11-22 10:20:45 -07:00
} else {
// use PR's commit messages as squash commit message
// commits list is in reverse chronological order
maxMsgSize := setting . Repository . PullRequest . DefaultMergeMessageSize
for i := len ( commits ) - 1 ; i >= 0 ; i -- {
commit := commits [ i ]
msg := strings . TrimSpace ( commit . CommitMessage )
if msg == "" {
continue
}
2021-06-18 17:08:22 -05:00
2025-11-22 10:20:45 -07:00
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_ , _ = fmt . Fprintf ( & stringBuilder , "* %s\n\n" , msg )
if maxMsgSize > 0 && stringBuilder . Len ( ) >= maxMsgSize {
tmp := stringBuilder . String ( )
wasValidUtf8 := utf8 . ValidString ( tmp )
tmp = tmp [ : maxMsgSize ] + "..."
if wasValidUtf8 {
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
tmp = strings . ToValidUTF8 ( tmp , "" )
2021-06-18 17:08:22 -05:00
}
2025-11-22 10:20:45 -07:00
stringBuilder . Reset ( )
stringBuilder . WriteString ( tmp )
break
2021-06-18 17:08:22 -05:00
}
}
2025-11-22 10:20:45 -07:00
}
2021-06-18 17:08:22 -05:00
2025-11-22 10:20:45 -07:00
// collect co-authors
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
// Compare use account as well to avoid adding the same author multiple times
2025-11-22 10:20:45 -07:00
// when email addresses are private or multiple emails are used.
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
}
}
2025-11-22 10:20:45 -07:00
// collect the remaining authors
2020-04-10 13:26:37 +02:00
if limit >= 0 && setting . Repository . PullRequest . DefaultMergeMessageAllAuthors {
skip := limit
limit = 30
for {
2025-11-22 10:20:45 -07: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 14:00:52 -06:00
skip += limit
2020-04-10 13:26:37 +02:00
}
}
2025-11-22 10:20:45 -07:00
if stringBuilder . Len ( ) > 0 && len ( authors ) > 0 {
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above
stringBuilder . WriteString ( "---------\n\n" )
}
2020-04-10 13:26:37 +02:00
for _ , author := range authors {
2025-11-22 10:20:45 -07:00
stringBuilder . WriteString ( "Co-authored-by: " )
stringBuilder . WriteString ( author )
stringBuilder . WriteRune ( '\n' )
2020-04-10 13:26:37 +02:00
}
return stringBuilder . String ( )
}
2022-04-26 17:40:01 -05:00
// 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 17:37:59 +08: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-26 17:40:01 -05:00
return nil , nil , err
2021-04-16 01:34:43 +08:00
}
2022-11-19 09:12:33 +01:00
if _ , err := issues . LoadRepositories ( ctx ) ; err != nil {
2022-04-26 17:40:01 -05:00
return nil , nil , err
2021-04-16 01:34:43 +08:00
}
var (
gitRepos = make ( map [ int64 ] * git . Repository )
2022-06-12 23:51:54 +08:00
res = make ( map [ int64 ] [ ] * git_model . CommitStatus )
lastRes = make ( map [ int64 ] * git_model . CommitStatus )
2021-04-16 01:34:43 +08: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 {
2024-01-28 04:09:51 +08:00
gitRepo , err = gitrepo . OpenRepository ( ctx , issue . Repo )
2021-04-16 01:34:43 +08:00
if err != nil {
2021-12-16 19:01:14 +00:00
log . Error ( "Cannot open git repository %-v for issue #%d[%d]. Error: %v" , issue . Repo , issue . Index , issue . ID , err )
continue
2021-04-16 01:34:43 +08:00
}
gitRepos [ issue . RepoID ] = gitRepo
}
2023-10-14 10:37:24 +02:00
statuses , lastStatus , err := getAllCommitStatus ( ctx , gitRepo , issue . PullRequest )
2021-04-16 01:34:43 +08:00
if err != nil {
2022-04-26 17:40:01 -05: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-16 01:34:43 +08:00
}
2022-04-26 17:40:01 -05:00
res [ issue . PullRequest . ID ] = statuses
lastRes [ issue . PullRequest . ID ] = lastStatus
2021-04-16 01:34:43 +08:00
}
2022-04-26 17:40:01 -05:00
return res , lastRes , nil
2021-04-16 01:34:43 +08:00
}
2022-04-26 17:40:01 -05:00
// getAllCommitStatus get pr's commit statuses.
2023-10-14 10:37:24 +02:00
func getAllCommitStatus ( ctx context . Context , gitRepo * git . Repository , pr * issues_model . PullRequest ) ( statuses [ ] * git_model . CommitStatus , lastStatus * git_model . CommitStatus , err error ) {
2025-07-16 21:33:33 +08:00
sha , shaErr := gitRepo . GetRefCommitID ( pr . GetGitHeadRefName ( ) )
2022-04-26 17:40:01 -05:00
if shaErr != nil {
return nil , nil , shaErr
2020-04-10 13:26:37 +02:00
}
2025-05-27 03:00:22 +08:00
statuses , err = git_model . GetLatestCommitStatus ( ctx , pr . BaseRepo . ID , sha , db . ListOptionsAll )
2022-06-12 23:51:54 +08:00
lastStatus = git_model . CalcCommitStatus ( statuses )
2022-04-26 17:40:01 -05: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 17:37:59 +08: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
}
2024-01-28 04:09:51 +08:00
baseGitRepo , closer , err := gitrepo . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo )
2020-04-10 13:26:37 +02:00
if err != nil {
return false , err
}
2022-01-19 23:26:57 +00:00
defer closer . Close ( )
2021-01-07 03:23:57 +08: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-19 23:26:57 +00:00
var headGitRepo * git . Repository
if pr . HeadRepoID == pr . BaseRepoID {
headGitRepo = baseGitRepo
} else {
var closer io . Closer
2024-01-28 04:09:51 +08:00
headGitRepo , closer , err = gitrepo . RepositoryFromContextOrOpen ( ctx , pr . HeadRepo )
2022-01-19 23:26:57 +00:00
if err != nil {
return false , err
}
defer closer . Close ( )
2020-04-10 13:26:37 +02:00
}
2021-01-07 03:23:57 +08:00
2021-07-28 17:42:56 +08:00
var headCommit * git . Commit
2022-06-13 17:37:59 +08:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2021-07-28 17:42:56 +08:00
headCommit , err = headGitRepo . GetBranchCommit ( pr . HeadBranch )
if err != nil {
return false , err
}
} else {
2025-07-16 21:33:33 +08:00
pr . HeadCommitID , err = baseGitRepo . GetRefCommitID ( pr . GetGitHeadRefName ( ) )
2021-07-28 17:42:56 +08:00
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 )
}
2023-07-28 21:18:12 +02:00
type CommitInfo struct {
Summary string ` json:"summary" `
CommitterOrAuthorName string ` json:"committer_or_author_name" `
ID string ` json:"id" `
ShortSha string ` json:"short_sha" `
Time string ` json:"time" `
}
// GetPullCommits returns all commits on given pull request and the last review commit sha
2024-10-01 09:58:55 +08:00
// Attention: The last review commit sha must be from the latest review whose commit id is not empty.
// So the type of the latest review cannot be "ReviewTypeRequest".
2025-07-31 11:43:54 +08:00
func GetPullCommits ( ctx context . Context , baseGitRepo * git . Repository , doer * user_model . User , issue * issues_model . Issue ) ( [ ] CommitInfo , string , error ) {
2023-07-28 21:18:12 +02:00
pull := issue . PullRequest
if err := pull . LoadBaseRepo ( ctx ) ; err != nil {
return nil , "" , err
}
baseBranch := pull . BaseBranch
if pull . HasMerged {
baseBranch = pull . MergeBase
}
2026-01-14 08:56:23 -08:00
compareInfo , err := git_service . GetCompareInfo ( ctx , pull . BaseRepo , pull . BaseRepo , baseGitRepo , git . RefNameFromBranch ( baseBranch ) , git . RefName ( pull . GetGitHeadRefName ( ) ) , false , false )
2023-07-28 21:18:12 +02:00
if err != nil {
return nil , "" , err
}
2025-12-25 17:51:30 -08:00
commits := make ( [ ] CommitInfo , 0 , len ( compareInfo . Commits ) )
2023-07-28 21:18:12 +02:00
2025-12-25 17:51:30 -08:00
for _ , commit := range compareInfo . Commits {
2023-07-28 21:18:12 +02:00
var committerOrAuthorName string
var commitTime time . Time
2024-04-05 02:51:53 +02:00
if commit . Author != nil {
2023-07-28 21:18:12 +02:00
committerOrAuthorName = commit . Author . Name
commitTime = commit . Author . When
2024-04-05 02:51:53 +02:00
} else {
committerOrAuthorName = commit . Committer . Name
commitTime = commit . Committer . When
2023-07-28 21:18:12 +02:00
}
commits = append ( commits , CommitInfo {
Summary : commit . Summary ( ) ,
CommitterOrAuthorName : committerOrAuthorName ,
ID : commit . ID . String ( ) ,
ShortSha : base . ShortSha ( commit . ID . String ( ) ) ,
Time : commitTime . Format ( time . RFC3339 ) ,
} )
}
var lastReviewCommitID string
2025-07-31 11:43:54 +08:00
if doer != nil {
2023-07-28 21:18:12 +02:00
// get last review of current user and store information in context (if available)
lastreview , err := issues_model . FindLatestReviews ( ctx , issues_model . FindReviewOptions {
IssueID : issue . ID ,
2025-07-31 11:43:54 +08:00
ReviewerID : doer . ID ,
2024-10-01 09:58:55 +08:00
Types : [ ] issues_model . ReviewType {
issues_model . ReviewTypeApprove ,
issues_model . ReviewTypeComment ,
issues_model . ReviewTypeReject ,
} ,
2023-07-28 21:18:12 +02:00
} )
if err != nil && ! issues_model . IsErrReviewNotExist ( err ) {
return nil , "" , err
}
if len ( lastreview ) > 0 {
lastReviewCommitID = lastreview [ 0 ] . CommitID
}
}
return commits , lastReviewCommitID , nil
}