2015-10-18 19:30:39 -04:00
// Copyright 2015 The Gogs Authors. All rights reserved.
2019-04-19 14:17:27 +02:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2015-10-18 19:30:39 -04:00
2022-06-13 17:37:59 +08:00
package issues
2015-10-18 19:30:39 -04:00
import (
2021-12-10 09:27:50 +08:00
"context"
2015-10-18 19:30:39 -04:00
"fmt"
2019-12-30 23:34:11 +00:00
"io"
2023-06-08 11:56:05 +03:00
"regexp"
2023-02-03 23:11:48 +00:00
"strconv"
2015-10-18 19:30:39 -04:00
"strings"
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"
2023-06-08 11:56:05 +03:00
org_model "code.gitea.io/gitea/models/organization"
2022-05-08 15:46:34 +02:00
pull_model "code.gitea.io/gitea/models/pull"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2021-12-02 08:28:08 +01:00
"code.gitea.io/gitea/modules/git"
2024-01-28 04:09:51 +08:00
"code.gitea.io/gitea/modules/gitrepo"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2021-02-18 03:45:49 +01:00
"code.gitea.io/gitea/modules/util"
2022-05-07 19:05:52 +02:00
"xorm.io/builder"
2015-10-18 19:30:39 -04:00
)
2022-06-13 17:37:59 +08:00
// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
type ErrPullRequestNotExist struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
func IsErrPullRequestNotExist ( err error ) bool {
_ , ok := err . ( ErrPullRequestNotExist )
return ok
}
func ( err ErrPullRequestNotExist ) Error ( ) string {
return fmt . Sprintf ( "pull request does not exist [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 )
}
2022-10-18 06:50:37 +01:00
func ( err ErrPullRequestNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-06-13 17:37:59 +08:00
// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
type ErrPullRequestAlreadyExists struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
func IsErrPullRequestAlreadyExists ( err error ) bool {
_ , ok := err . ( ErrPullRequestAlreadyExists )
return ok
}
// Error does pretty-printing :D
func ( err ErrPullRequestAlreadyExists ) Error ( ) string {
return fmt . Sprintf ( "pull request already exists for these targets [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 )
}
2022-10-18 06:50:37 +01:00
func ( err ErrPullRequestAlreadyExists ) Unwrap ( ) error {
return util . ErrAlreadyExist
}
2022-06-13 17:37:59 +08:00
// ErrPullWasClosed is used close a closed pull request
type ErrPullWasClosed struct {
ID int64
Index int64
}
// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
func IsErrPullWasClosed ( err error ) bool {
_ , ok := err . ( ErrPullWasClosed )
return ok
}
func ( err ErrPullWasClosed ) Error ( ) string {
return fmt . Sprintf ( "Pull request [%d] %d was already closed" , err . ID , err . Index )
}
2016-11-28 23:31:06 +08:00
// PullRequestType defines pull request type
2015-10-18 19:30:39 -04:00
type PullRequestType int
2016-11-28 23:31:06 +08:00
// Enumerate all the pull request types
2015-10-18 19:30:39 -04:00
const (
2016-11-07 16:37:32 +01:00
PullRequestGitea PullRequestType = iota
PullRequestGit
2015-10-18 19:30:39 -04:00
)
2016-11-28 23:31:06 +08:00
// PullRequestStatus defines pull request status
2015-10-18 19:30:39 -04:00
type PullRequestStatus int
2016-11-28 23:31:06 +08:00
// Enumerate all the pull request status
2015-10-18 19:30:39 -04:00
const (
2016-11-07 16:37:32 +01:00
PullRequestStatusConflict PullRequestStatus = iota
PullRequestStatusChecking
PullRequestStatusMergeable
2017-02-05 14:07:44 +01:00
PullRequestStatusManuallyMerged
2020-01-10 00:14:14 +00:00
PullRequestStatusError
2021-03-04 11:41:23 +08:00
PullRequestStatusEmpty
2022-07-13 10:22:51 +02:00
PullRequestStatusAncestor
2015-10-18 19:30:39 -04:00
)
2023-02-03 23:11:48 +00:00
func ( status PullRequestStatus ) String ( ) string {
switch status {
case PullRequestStatusConflict :
return "CONFLICT"
case PullRequestStatusChecking :
return "CHECKING"
case PullRequestStatusMergeable :
return "MERGEABLE"
case PullRequestStatusManuallyMerged :
return "MANUALLY_MERGED"
case PullRequestStatusError :
return "ERROR"
case PullRequestStatusEmpty :
return "EMPTY"
case PullRequestStatusAncestor :
return "ANCESTOR"
default :
return strconv . Itoa ( int ( status ) )
}
}
2021-07-28 17:42:56 +08:00
// PullRequestFlow the flow of pull request
type PullRequestFlow int
const (
// PullRequestFlowGithub github flow from head branch to base branch
PullRequestFlowGithub PullRequestFlow = iota
// PullRequestFlowAGit Agit flow pull request, head branch is not exist
PullRequestFlowAGit
)
2015-10-18 19:30:39 -04:00
// PullRequest represents relation between pull request and repositories.
type PullRequest struct {
2019-02-05 19:54:49 +08:00
ID int64 ` xorm:"pk autoincr" `
Type PullRequestType
Status PullRequestStatus
ConflictedFiles [ ] string ` xorm:"TEXT JSON" `
2020-04-14 15:53:34 +02:00
CommitsAhead int
CommitsBehind int
2015-10-18 19:30:39 -04:00
2020-10-14 02:50:57 +08:00
ChangedProtectedFiles [ ] string ` xorm:"TEXT JSON" `
2023-05-25 10:06:27 +08:00
IssueID int64 ` xorm:"INDEX" `
Issue * Issue ` xorm:"-" `
Index int64
RequestedReviewers [ ] * user_model . User ` xorm:"-" `
2015-10-18 19:30:39 -04:00
2022-04-28 17:45:33 +02:00
HeadRepoID int64 ` xorm:"INDEX" `
HeadRepo * repo_model . Repository ` xorm:"-" `
BaseRepoID int64 ` xorm:"INDEX" `
BaseRepo * repo_model . Repository ` xorm:"-" `
HeadBranch string
HeadCommitID string ` xorm:"-" `
BaseBranch string
2024-01-19 16:05:02 +00:00
MergeBase string ` xorm:"VARCHAR(64)" `
2023-01-16 16:00:22 +08:00
AllowMaintainerEdit bool ` xorm:"NOT NULL DEFAULT false" `
2015-10-18 19:30:39 -04:00
2019-08-15 22:46:21 +08:00
HasMerged bool ` xorm:"INDEX" `
2024-01-19 16:05:02 +00:00
MergedCommitID string ` xorm:"VARCHAR(64)" `
2019-08-15 22:46:21 +08:00
MergerID int64 ` xorm:"INDEX" `
2021-11-24 17:49:20 +08:00
Merger * user_model . User ` xorm:"-" `
2019-08-15 22:46:21 +08:00
MergedUnix timeutil . TimeStamp ` xorm:"updated INDEX" `
2020-03-03 06:31:55 +08:00
isHeadRepoLoaded bool ` xorm:"-" `
2021-07-28 17:42:56 +08:00
Flow PullRequestFlow ` xorm:"NOT NULL DEFAULT 0" `
2015-10-18 19:30:39 -04:00
}
2021-09-19 19:49:59 +08:00
func init ( ) {
db . RegisterModel ( new ( PullRequest ) )
}
2022-06-13 17:37:59 +08:00
// DeletePullsByBaseRepoID deletes all pull requests by the base repository ID
func DeletePullsByBaseRepoID ( ctx context . Context , repoID int64 ) error {
2022-05-08 15:46:34 +02:00
deleteCond := builder . Select ( "id" ) . From ( "pull_request" ) . Where ( builder . Eq { "pull_request.base_repo_id" : repoID } )
// Delete scheduled auto merges
2022-05-20 22:08:52 +08:00
if _ , err := db . GetEngine ( ctx ) . In ( "pull_id" , deleteCond ) .
2022-05-08 15:46:34 +02:00
Delete ( & pull_model . AutoMerge { } ) ; err != nil {
return err
}
// Delete review states
2022-05-20 22:08:52 +08:00
if _ , err := db . GetEngine ( ctx ) . In ( "pull_id" , deleteCond ) .
2022-05-08 15:46:34 +02:00
Delete ( & pull_model . ReviewState { } ) ; err != nil {
return err
}
2022-05-20 22:08:52 +08:00
_ , err := db . DeleteByBean ( ctx , & PullRequest { BaseRepoID : repoID } )
2022-05-08 15:46:34 +02:00
return err
}
2023-05-22 06:35:11 +08:00
func ( pr * PullRequest ) String ( ) string {
2023-02-03 23:11:48 +00:00
if pr == nil {
2023-05-22 06:35:11 +08:00
return "<PullRequest nil>"
2023-02-03 23:11:48 +00:00
}
2023-05-22 06:35:11 +08:00
s := new ( strings . Builder )
fmt . Fprintf ( s , "<PullRequest [%d]" , pr . ID )
2023-02-03 23:11:48 +00:00
if pr . BaseRepo != nil {
2023-05-22 06:35:11 +08:00
fmt . Fprintf ( s , "%s#%d[%s..." , pr . BaseRepo . FullName ( ) , pr . Index , pr . BaseBranch )
2023-02-03 23:11:48 +00:00
} else {
2023-05-22 06:35:11 +08:00
fmt . Fprintf ( s , "Repo[%d]#%d[%s..." , pr . BaseRepoID , pr . Index , pr . BaseBranch )
2023-02-03 23:11:48 +00:00
}
if pr . HeadRepoID == pr . BaseRepoID {
2023-05-22 06:35:11 +08:00
fmt . Fprintf ( s , "%s]" , pr . HeadBranch )
2023-02-03 23:11:48 +00:00
} else if pr . HeadRepo != nil {
2023-05-22 06:35:11 +08:00
fmt . Fprintf ( s , "%s:%s]" , pr . HeadRepo . FullName ( ) , pr . HeadBranch )
2023-02-03 23:11:48 +00:00
} else {
2023-05-22 06:35:11 +08:00
fmt . Fprintf ( s , "Repo[%d]:%s]" , pr . HeadRepoID , pr . HeadBranch )
2023-02-03 23:11:48 +00:00
}
2023-05-22 06:35:11 +08:00
s . WriteByte ( '>' )
return s . String ( )
2023-02-03 23:11:48 +00:00
}
2019-10-18 19:13:31 +08:00
// MustHeadUserName returns the HeadRepo's username if failed return blank
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) MustHeadUserName ( ctx context . Context ) string {
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2021-12-10 09:27:50 +08:00
if ! repo_model . IsErrRepoNotExist ( err ) {
2020-01-25 10:48:22 +08:00
log . Error ( "LoadHeadRepo: %v" , err )
} else {
log . Warn ( "LoadHeadRepo %d but repository does not exist: %v" , pr . HeadRepoID , err )
}
2019-10-18 19:13:31 +08:00
return ""
}
2020-03-03 06:31:55 +08:00
if pr . HeadRepo == nil {
return ""
}
2020-01-12 17:36:21 +08:00
return pr . HeadRepo . OwnerName
2019-10-18 19:13:31 +08:00
}
2022-11-19 09:12:33 +01:00
// LoadAttributes loads pull request attributes from database
2016-08-14 03:32:24 -07:00
// Note: don't try to get Issue because will end up recursive querying.
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) LoadAttributes ( ctx context . Context ) ( err error ) {
2016-08-14 03:32:24 -07:00
if pr . HasMerged && pr . Merger == nil {
2022-12-03 10:48:26 +08:00
pr . Merger , err = user_model . GetUserByID ( ctx , pr . MergerID )
2021-11-24 17:49:20 +08:00
if user_model . IsErrUserNotExist ( err ) {
2023-10-20 22:43:08 +08:00
pr . MergerID = user_model . GhostUserID
2021-11-24 17:49:20 +08:00
pr . Merger = user_model . NewGhostUser ( )
2016-08-14 03:32:24 -07:00
} else if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "getUserByID [%d]: %w" , pr . MergerID , err )
2016-08-14 03:32:24 -07:00
}
}
return nil
}
2023-02-03 23:11:48 +00:00
// LoadHeadRepo loads the head repository, pr.HeadRepo will remain nil if it does not exist
// and thus ErrRepoNotExist will never be returned
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) LoadHeadRepo ( ctx context . Context ) ( err error ) {
2020-03-03 06:31:55 +08:00
if ! pr . isHeadRepoLoaded && pr . HeadRepo == nil && pr . HeadRepoID > 0 {
if pr . HeadRepoID == pr . BaseRepoID {
if pr . BaseRepo != nil {
pr . HeadRepo = pr . BaseRepo
return nil
} else if pr . Issue != nil && pr . Issue . Repo != nil {
pr . HeadRepo = pr . Issue . Repo
return nil
}
2019-10-18 19:13:31 +08:00
}
2020-03-03 06:31:55 +08:00
2022-12-03 10:48:26 +08:00
pr . HeadRepo , err = repo_model . GetRepositoryByID ( ctx , pr . HeadRepoID )
2021-12-10 09:27:50 +08:00
if err != nil && ! repo_model . IsErrRepoNotExist ( err ) { // Head repo maybe deleted, but it should still work
2023-02-03 23:11:48 +00:00
return fmt . Errorf ( "pr[%d].LoadHeadRepo[%d]: %w" , pr . ID , pr . HeadRepoID , err )
2019-09-18 13:39:45 +08:00
}
2020-03-03 06:31:55 +08:00
pr . isHeadRepoLoaded = true
2019-09-18 13:39:45 +08:00
}
return nil
}
2023-05-25 10:06:27 +08:00
// LoadRequestedReviewers loads the requested reviewers.
func ( pr * PullRequest ) LoadRequestedReviewers ( ctx context . Context ) error {
if len ( pr . RequestedReviewers ) > 0 {
return nil
}
2023-09-25 15:17:37 +02:00
reviews , err := GetReviewsByIssueID ( ctx , pr . Issue . ID )
2023-05-25 10:06:27 +08:00
if err != nil {
return err
}
2023-07-20 15:18:52 +08:00
if err = reviews . LoadReviewers ( ctx ) ; err != nil {
return err
2023-05-25 10:06:27 +08:00
}
2023-07-20 15:18:52 +08:00
for _ , review := range reviews {
pr . RequestedReviewers = append ( pr . RequestedReviewers , review . Reviewer )
}
2023-05-25 10:06:27 +08:00
return nil
}
2023-02-03 23:11:48 +00:00
// LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned.
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) LoadBaseRepo ( ctx context . Context ) ( err error ) {
2020-03-03 06:31:55 +08:00
if pr . BaseRepo != nil {
return nil
}
if pr . HeadRepoID == pr . BaseRepoID && pr . HeadRepo != nil {
pr . BaseRepo = pr . HeadRepo
return nil
}
if pr . Issue != nil && pr . Issue . Repo != nil {
pr . BaseRepo = pr . Issue . Repo
return nil
}
2022-12-03 10:48:26 +08:00
pr . BaseRepo , err = repo_model . GetRepositoryByID ( ctx , pr . BaseRepoID )
2020-03-03 06:31:55 +08:00
if err != nil {
2023-02-03 23:11:48 +00:00
return fmt . Errorf ( "pr[%d].LoadBaseRepo[%d]: %w" , pr . ID , pr . BaseRepoID , err )
2019-10-18 19:13:31 +08:00
}
return nil
}
2016-11-28 23:31:06 +08:00
// LoadIssue loads issue information from database
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) LoadIssue ( ctx context . Context ) ( err error ) {
2016-08-16 10:19:09 -07:00
if pr . Issue != nil {
return nil
}
2022-06-13 17:37:59 +08:00
pr . Issue , err = GetIssueByID ( ctx , pr . IssueID )
2019-11-22 01:08:42 +08:00
if err == nil {
pr . Issue . PullRequest = pr
}
2016-08-14 03:32:24 -07:00
return err
}
2020-03-06 03:44:06 +00:00
// ReviewCount represents a count of Reviews
type ReviewCount struct {
IssueID int64
Type ReviewType
Count int64
}
// GetApprovalCounts returns the approval counts by type
// FIXME: Only returns official counts due to double counting of non-official counts
2022-05-20 22:08:52 +08:00
func ( pr * PullRequest ) GetApprovalCounts ( ctx context . Context ) ( [ ] * ReviewCount , error ) {
2020-03-06 03:44:06 +00:00
rCounts := make ( [ ] * ReviewCount , 0 , 6 )
2022-05-20 22:08:52 +08:00
sess := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID )
2021-02-12 01:32:25 +08:00
return rCounts , sess . Select ( "issue_id, type, count(id) as `count`" ) . Where ( "official = ? AND dismissed = ?" , true , false ) . GroupBy ( "issue_id, type" ) . Table ( "review" ) . Find ( & rCounts )
2020-03-06 03:44:06 +00:00
}
2019-12-30 23:34:11 +00:00
// GetApprovers returns the approvers of the pull request
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) GetApprovers ( ctx context . Context ) string {
2019-12-30 23:34:11 +00:00
stringBuilder := strings . Builder { }
2023-10-11 06:24:07 +02:00
if err := pr . getReviewedByLines ( ctx , & stringBuilder ) ; err != nil {
2019-12-30 23:34:11 +00:00
log . Error ( "Unable to getReviewedByLines: Error: %v" , err )
return ""
}
return stringBuilder . String ( )
}
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) getReviewedByLines ( ctx context . Context , writer io . Writer ) error {
2019-12-30 23:34:11 +00:00
maxReviewers := setting . Repository . PullRequest . DefaultMergeMessageMaxApprovers
if maxReviewers == 0 {
return nil
}
2023-10-11 06:24:07 +02:00
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 23:41:00 +08:00
if err != nil {
2019-12-30 23:34:11 +00:00
return err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
2019-12-30 23:34:11 +00:00
// Note: This doesn't page as we only expect a very limited number of reviews
2023-06-09 07:34:49 -07:00
reviews , err := FindLatestReviews ( ctx , FindReviewOptions {
2019-12-30 23:34:11 +00:00
Type : ReviewTypeApprove ,
IssueID : pr . IssueID ,
OfficialOnly : setting . Repository . PullRequest . DefaultMergeMessageOfficialApproversOnly ,
} )
if err != nil {
log . Error ( "Unable to FindReviews for PR ID %d: %v" , pr . ID , err )
return err
}
reviewersWritten := 0
for _ , review := range reviews {
if maxReviewers > 0 && reviewersWritten > maxReviewers {
break
}
2022-11-19 09:12:33 +01:00
if err := review . LoadReviewer ( ctx ) ; err != nil && ! user_model . IsErrUserNotExist ( err ) {
2019-12-30 23:34:11 +00:00
log . Error ( "Unable to LoadReviewer[%d] for PR ID %d : %v" , review . ReviewerID , pr . ID , err )
return err
} else if review . Reviewer == nil {
continue
}
if _ , err := writer . Write ( [ ] byte ( "Reviewed-by: " ) ) ; err != nil {
return err
}
if _ , err := writer . Write ( [ ] byte ( review . Reviewer . NewGitSig ( ) . String ( ) ) ) ; err != nil {
return err
}
if _ , err := writer . Write ( [ ] byte { '\n' } ) ; err != nil {
return err
}
reviewersWritten ++
}
2021-11-21 23:41:00 +08:00
return committer . Commit ( )
2019-12-30 23:34:11 +00:00
}
2018-01-19 08:18:51 +02:00
// GetGitRefName returns git ref for hidden pull request branch
func ( pr * PullRequest ) GetGitRefName ( ) string {
2021-12-02 20:36:50 +01:00
return fmt . Sprintf ( "%s%d/head" , git . PullPrefix , pr . Index )
2018-01-19 08:18:51 +02:00
}
2023-05-08 14:39:32 +08:00
func ( pr * PullRequest ) GetGitHeadBranchRefName ( ) string {
return fmt . Sprintf ( "%s%s" , git . BranchPrefix , pr . HeadBranch )
}
2015-10-24 03:36:47 -04:00
// IsChecking returns true if this pull request is still checking conflict.
func ( pr * PullRequest ) IsChecking ( ) bool {
2016-11-07 16:37:32 +01:00
return pr . Status == PullRequestStatusChecking
2015-10-24 03:36:47 -04:00
}
2015-10-18 19:30:39 -04:00
// CanAutoMerge returns true if this pull request can be merged automatically.
func ( pr * PullRequest ) CanAutoMerge ( ) bool {
2016-11-07 16:37:32 +01:00
return pr . Status == PullRequestStatusMergeable
2015-10-18 19:30:39 -04:00
}
2021-03-04 11:41:23 +08:00
// IsEmpty returns true if this pull request is empty.
func ( pr * PullRequest ) IsEmpty ( ) bool {
return pr . Status == PullRequestStatusEmpty
}
2022-07-13 10:22:51 +02:00
// IsAncestor returns true if the Head Commit of this PR is an ancestor of the Base Commit
func ( pr * PullRequest ) IsAncestor ( ) bool {
return pr . Status == PullRequestStatusAncestor
}
2023-01-31 09:45:19 +08:00
// IsFromFork return true if this PR is from a fork.
func ( pr * PullRequest ) IsFromFork ( ) bool {
return pr . HeadRepoID != pr . BaseRepoID
}
2019-06-22 18:35:34 +01:00
// SetMerged sets a pull request to merged and closes the corresponding issue
2022-05-03 21:46:28 +02:00
func ( pr * PullRequest ) SetMerged ( ctx context . Context ) ( bool , error ) {
2017-02-05 14:07:44 +01:00
if pr . HasMerged {
2020-02-09 23:09:31 +00:00
return false , fmt . Errorf ( "PullRequest[%d] already merged" , pr . Index )
2017-02-05 14:07:44 +01:00
}
2017-12-11 12:37:04 +08:00
if pr . MergedCommitID == "" || pr . MergedUnix == 0 || pr . Merger == nil {
2020-02-09 23:09:31 +00:00
return false , fmt . Errorf ( "Unable to merge PullRequest[%d], some required fields are empty" , pr . Index )
2017-02-05 14:07:44 +01:00
}
pr . HasMerged = true
2021-11-19 21:39:57 +08:00
sess := db . GetEngine ( ctx )
2017-02-05 14:07:44 +01:00
2020-02-09 23:09:31 +00:00
if _ , err := sess . Exec ( "UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?" , pr . IssueID ) ; err != nil {
return false , err
2017-02-05 14:07:44 +01:00
}
2020-02-09 23:09:31 +00:00
if _ , err := sess . Exec ( "UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?" , pr . ID ) ; err != nil {
return false , err
2017-02-27 08:42:55 +08:00
}
2020-02-09 23:09:31 +00:00
pr . Issue = nil
2022-11-19 09:12:33 +01:00
if err := pr . LoadIssue ( ctx ) ; err != nil {
2020-02-09 23:09:31 +00:00
return false , err
2017-02-05 14:07:44 +01:00
}
2022-05-20 22:08:52 +08:00
if tmpPr , err := GetPullRequestByID ( ctx , pr . ID ) ; err != nil {
2020-02-09 23:09:31 +00:00
return false , err
} else if tmpPr . HasMerged {
if pr . Issue . IsClosed {
return false , nil
}
return false , fmt . Errorf ( "PullRequest[%d] already merged but it's associated issue [%d] is not closed" , pr . Index , pr . IssueID )
} else if pr . Issue . IsClosed {
return false , fmt . Errorf ( "PullRequest[%d] already closed" , pr . Index )
2017-02-05 14:07:44 +01:00
}
2020-02-09 23:09:31 +00:00
2022-04-08 17:11:15 +08:00
if err := pr . Issue . LoadRepo ( ctx ) ; err != nil {
2020-02-09 23:09:31 +00:00
return false , err
2017-02-05 14:07:44 +01:00
}
2023-02-18 21:11:03 +09:00
if err := pr . Issue . Repo . LoadOwner ( ctx ) ; err != nil {
2020-02-09 23:09:31 +00:00
return false , err
2017-02-05 14:07:44 +01:00
}
2020-02-09 23:09:31 +00:00
2022-03-29 22:57:33 +08:00
if _ , err := changeIssueStatus ( ctx , pr . Issue , pr . Merger , true , true ) ; err != nil {
2022-10-24 21:29:17 +02:00
return false , fmt . Errorf ( "Issue.changeStatus: %w" , err )
2020-02-09 23:09:31 +00:00
}
2022-03-29 17:42:34 +01:00
// reset the conflicted files as there cannot be any if we're merged
pr . ConflictedFiles = [ ] string { }
2021-04-10 09:27:29 +01:00
// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
2022-03-29 17:42:34 +01:00
if _ , err := sess . Where ( "id = ?" , pr . ID ) . Cols ( "has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files" ) . Update ( pr ) ; err != nil {
2022-10-24 21:29:17 +02:00
return false , fmt . Errorf ( "Failed to update pr[%d]: %w" , pr . ID , err )
2020-02-09 23:09:31 +00:00
}
return true , nil
2017-02-05 14:07:44 +01:00
}
2015-10-18 19:30:39 -04:00
// NewPullRequest creates new pull request with labels for repository.
2023-08-10 10:39:21 +08:00
func NewPullRequest ( ctx context . Context , repo * repo_model . Repository , issue * Issue , labelIDs [ ] int64 , uuids [ ] string , pr * PullRequest ) ( err error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 21:39:57 +08:00
if err != nil {
2015-10-18 19:30:39 -04:00
return err
}
2021-11-19 21:39:57 +08:00
defer committer . Close ( )
2015-10-18 19:30:39 -04:00
2022-10-16 18:44:16 +08:00
idx , err := db . GetNextResourceIndex ( ctx , "issue_index" , repo . ID )
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "generate pull request index failed: %w" , err )
2022-10-16 18:44:16 +08:00
}
issue . Index = idx
2022-06-13 17:37:59 +08:00
if err = NewIssueWithIndex ( ctx , issue . Poster , NewIssueOptions {
2016-08-15 18:40:32 -07:00
Repo : repo ,
2021-06-14 10:22:55 +08:00
Issue : issue ,
2017-02-28 20:08:45 -05:00
LabelIDs : labelIDs ,
2016-08-15 18:40:32 -07:00
Attachments : uuids ,
IsPull : true ,
} ) ; err != nil {
2022-06-13 17:37:59 +08:00
if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) || IsErrNewIssueInsert ( err ) {
2018-05-09 18:29:04 +02:00
return err
}
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "newIssue: %w" , err )
2015-10-18 19:30:39 -04:00
}
2021-06-14 10:22:55 +08:00
pr . Index = issue . Index
2015-10-24 03:36:47 -04:00
pr . BaseRepo = repo
2021-06-14 10:22:55 +08:00
pr . IssueID = issue . ID
2021-11-19 21:39:57 +08:00
if err = db . Insert ( ctx , pr ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "insert pull repo: %w" , err )
2015-10-18 19:30:39 -04:00
}
2021-11-19 21:39:57 +08:00
if err = committer . Commit ( ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "Commit: %w" , err )
2016-07-16 00:36:39 +08:00
}
return nil
2015-10-18 19:30:39 -04:00
}
2017-01-04 19:50:34 -05:00
// GetUnmergedPullRequest returns a pull request that is open and has not been merged
2015-10-18 19:30:39 -04:00
// by given head/base and repo/branch.
2022-11-19 09:12:33 +01:00
func GetUnmergedPullRequest ( ctx context . Context , headRepoID , baseRepoID int64 , headBranch , baseBranch string , flow PullRequestFlow ) ( * PullRequest , error ) {
2015-10-18 19:30:39 -04:00
pr := new ( PullRequest )
2022-11-19 09:12:33 +01:00
has , err := db . GetEngine ( ctx ) .
2021-07-28 17:42:56 +08:00
Where ( "head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?" ,
headRepoID , headBranch , baseRepoID , baseBranch , false , flow , false ) .
2016-11-10 16:16:32 +01:00
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) .
Get ( pr )
2015-10-18 19:30:39 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { 0 , 0 , headRepoID , baseRepoID , headBranch , baseBranch }
}
return pr , nil
}
2019-06-27 16:15:30 +02:00
// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status)
// by given head information (repo and branch).
2023-10-11 06:24:07 +02:00
func GetLatestPullRequestByHeadInfo ( ctx context . Context , repoID int64 , branch string ) ( * PullRequest , error ) {
2019-06-27 16:15:30 +02:00
pr := new ( PullRequest )
2023-10-11 06:24:07 +02:00
has , err := db . GetEngine ( ctx ) .
2021-07-28 17:42:56 +08:00
Where ( "head_repo_id = ? AND head_branch = ? AND flow = ?" , repoID , branch , PullRequestFlowGithub ) .
2019-06-27 16:15:30 +02:00
OrderBy ( "id DESC" ) .
Get ( pr )
if ! has {
return nil , err
}
return pr , err
}
2016-12-02 12:10:39 +01:00
// GetPullRequestByIndex returns a pull request by the given index
2022-05-20 22:08:52 +08:00
func GetPullRequestByIndex ( ctx context . Context , repoID , index int64 ) ( * PullRequest , error ) {
2021-10-05 21:41:48 +07:00
if index < 1 {
return nil , ErrPullRequestNotExist { }
}
2016-12-02 12:10:39 +01:00
pr := & PullRequest {
BaseRepoID : repoID ,
Index : index ,
}
2022-04-28 13:48:48 +02:00
has , err := db . GetEngine ( ctx ) . Get ( pr )
2016-12-02 12:10:39 +01:00
if err != nil {
return nil , err
} else if ! has {
2017-01-01 13:15:09 -05:00
return nil , ErrPullRequestNotExist { 0 , 0 , 0 , repoID , "" , "" }
2016-12-02 12:10:39 +01:00
}
2022-11-19 09:12:33 +01:00
if err = pr . LoadAttributes ( ctx ) ; err != nil {
2016-12-05 12:17:39 +01:00
return nil , err
}
2022-11-19 09:12:33 +01:00
if err = pr . LoadIssue ( ctx ) ; err != nil {
2016-12-05 12:17:39 +01:00
return nil , err
}
2016-12-02 12:10:39 +01:00
return pr , nil
}
2022-05-20 22:08:52 +08:00
// GetPullRequestByID returns a pull request by given ID.
func GetPullRequestByID ( ctx context . Context , id int64 ) ( * PullRequest , error ) {
2015-10-24 03:36:47 -04:00
pr := new ( PullRequest )
2022-05-20 22:08:52 +08:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( pr )
2015-10-24 03:36:47 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrPullRequestNotExist { id , 0 , 0 , 0 , "" , "" }
}
2022-11-19 09:12:33 +01:00
return pr , pr . LoadAttributes ( ctx )
2016-08-14 03:32:24 -07:00
}
2020-01-24 01:28:15 +08:00
// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
2023-10-11 06:24:07 +02:00
func GetPullRequestByIssueIDWithNoAttributes ( ctx context . Context , issueID int64 ) ( * PullRequest , error ) {
2020-01-24 01:28:15 +08:00
var pr PullRequest
2023-10-11 06:24:07 +02:00
has , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , issueID ) . Get ( & pr )
2020-01-24 01:28:15 +08:00
if err != nil {
return nil , err
}
if ! has {
return nil , ErrPullRequestNotExist { 0 , issueID , 0 , 0 , "" , "" }
}
return & pr , nil
}
2022-05-20 22:08:52 +08:00
// GetPullRequestByIssueID returns pull request by given issue ID.
func GetPullRequestByIssueID ( ctx context . Context , issueID int64 ) ( * PullRequest , error ) {
2023-12-07 15:27:36 +08:00
pr , exist , err := db . Get [ PullRequest ] ( ctx , builder . Eq { "issue_id" : issueID } )
2015-10-18 19:30:39 -04:00
if err != nil {
return nil , err
2023-12-07 15:27:36 +08:00
} else if ! exist {
2015-10-22 14:47:24 -04:00
return nil , ErrPullRequestNotExist { 0 , issueID , 0 , 0 , "" , "" }
2015-10-18 19:30:39 -04:00
}
2022-11-19 09:12:33 +01:00
return pr , pr . LoadAttributes ( ctx )
2016-08-14 03:32:24 -07:00
}
2021-07-28 17:42:56 +08:00
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
// By poster id.
2023-03-14 03:45:21 -04:00
func GetAllUnmergedAgitPullRequestByPoster ( ctx context . Context , uid int64 ) ( [ ] * PullRequest , error ) {
2021-07-28 17:42:56 +08:00
pulls := make ( [ ] * PullRequest , 0 , 10 )
2023-03-14 03:45:21 -04:00
err := db . GetEngine ( ctx ) .
2021-07-28 17:42:56 +08:00
Where ( "has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?" ,
false , PullRequestFlowAGit , false , uid ) .
Join ( "INNER" , "issue" , "issue.id=pull_request.issue_id" ) .
Find ( & pulls )
return pulls , err
}
2015-10-24 03:36:47 -04:00
// Update updates all fields of pull request.
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) Update ( ctx context . Context ) error {
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . AllCols ( ) . Update ( pr )
2015-10-24 03:36:47 -04:00
return err
}
2016-11-28 23:31:06 +08:00
// UpdateCols updates specific fields of pull request.
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) UpdateCols ( ctx context . Context , cols ... string ) error {
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( cols ... ) . Update ( pr )
2015-10-24 03:36:47 -04:00
return err
2020-02-09 23:09:31 +00:00
}
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) UpdateColsIfNotMerged ( ctx context . Context , cols ... string ) error {
_ , err := db . GetEngine ( ctx ) . Where ( "id = ? AND has_merged = ?" , pr . ID , false ) . Cols ( cols ... ) . Update ( pr )
2020-02-09 23:09:31 +00:00
return err
2015-10-24 03:36:47 -04:00
}
2018-08-13 21:04:39 +02:00
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
2022-11-19 09:12:33 +01:00
// Issue must be set before this method can be called.
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) IsWorkInProgress ( ctx context . Context ) bool {
if err := pr . LoadIssue ( ctx ) ; err != nil {
2019-04-02 08:48:31 +01:00
log . Error ( "LoadIssue: %v" , err )
2018-08-13 21:04:39 +02:00
return false
}
2021-06-23 06:14:22 +02:00
return HasWorkInProgressPrefix ( pr . Issue . Title )
}
2018-08-13 21:04:39 +02:00
2021-06-23 06:14:22 +02:00
// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix
func HasWorkInProgressPrefix ( title string ) bool {
2018-08-13 21:04:39 +02:00
for _ , prefix := range setting . Repository . PullRequest . WorkInProgressPrefixes {
2022-05-26 03:19:24 -06:00
if strings . HasPrefix ( strings . ToUpper ( title ) , strings . ToUpper ( prefix ) ) {
2018-08-13 21:04:39 +02:00
return true
}
}
return false
}
2019-02-05 19:54:49 +08:00
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
func ( pr * PullRequest ) IsFilesConflicted ( ) bool {
return len ( pr . ConflictedFiles ) > 0
}
2018-08-13 21:04:39 +02:00
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
// It returns an empty string when none were found
2022-11-19 09:12:33 +01:00
func ( pr * PullRequest ) GetWorkInProgressPrefix ( ctx context . Context ) string {
if err := pr . LoadIssue ( ctx ) ; err != nil {
2019-04-02 08:48:31 +01:00
log . Error ( "LoadIssue: %v" , err )
2018-08-13 21:04:39 +02:00
return ""
}
for _ , prefix := range setting . Repository . PullRequest . WorkInProgressPrefixes {
2022-05-26 03:19:24 -06:00
if strings . HasPrefix ( strings . ToUpper ( pr . Issue . Title ) , strings . ToUpper ( prefix ) ) {
2018-08-13 21:04:39 +02:00
return pr . Issue . Title [ 0 : len ( prefix ) ]
}
}
return ""
}
2019-12-16 07:20:25 +01:00
2020-04-14 15:53:34 +02:00
// UpdateCommitDivergence update Divergence of a pull request
2022-05-20 22:08:52 +08:00
func ( pr * PullRequest ) UpdateCommitDivergence ( ctx context . Context , ahead , behind int ) error {
2020-04-14 15:53:34 +02:00
if pr . ID == 0 {
return fmt . Errorf ( "pull ID is 0" )
}
pr . CommitsAhead = ahead
pr . CommitsBehind = behind
2022-05-20 22:08:52 +08:00
_ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( "commits_ahead" , "commits_behind" ) . Update ( pr )
2020-04-14 15:53:34 +02:00
return err
}
2020-01-17 07:03:40 +01:00
// IsSameRepo returns true if base repo and head repo is the same
func ( pr * PullRequest ) IsSameRepo ( ) bool {
return pr . BaseRepoID == pr . HeadRepoID
}
2021-02-18 03:45:49 +01:00
2023-02-07 02:09:18 +08:00
// GetBaseBranchLink returns the relative URL of the base branch
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) GetBaseBranchLink ( ctx context . Context ) string {
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2021-02-18 03:45:49 +01:00
log . Error ( "LoadBaseRepo: %v" , err )
return ""
}
if pr . BaseRepo == nil {
return ""
}
2023-02-07 02:09:18 +08:00
return pr . BaseRepo . Link ( ) + "/src/branch/" + util . PathEscapeSegments ( pr . BaseBranch )
2021-02-18 03:45:49 +01:00
}
2023-02-07 02:09:18 +08:00
// GetHeadBranchLink returns the relative URL of the head branch
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) GetHeadBranchLink ( ctx context . Context ) string {
2021-07-28 17:42:56 +08:00
if pr . Flow == PullRequestFlowAGit {
return ""
}
2023-10-11 06:24:07 +02:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2021-02-18 03:45:49 +01:00
log . Error ( "LoadHeadRepo: %v" , err )
return ""
}
if pr . HeadRepo == nil {
return ""
}
2023-02-07 02:09:18 +08:00
return pr . HeadRepo . Link ( ) + "/src/branch/" + util . PathEscapeSegments ( pr . HeadBranch )
2021-02-18 03:45:49 +01:00
}
2022-04-21 21:55:45 +00:00
2022-04-28 17:45:33 +02:00
// UpdateAllowEdits update if PR can be edited from maintainers
func UpdateAllowEdits ( ctx context . Context , pr * PullRequest ) error {
if _ , err := db . GetEngine ( ctx ) . ID ( pr . ID ) . Cols ( "allow_maintainer_edit" ) . Update ( pr ) ; err != nil {
return err
}
return nil
}
2022-04-21 21:55:45 +00:00
// Mergeable returns if the pullrequest is mergeable.
2023-10-11 06:24:07 +02:00
func ( pr * PullRequest ) Mergeable ( ctx context . Context ) bool {
2022-04-21 21:55:45 +00:00
// If a pull request isn't mergable if it's:
// - Being conflict checked.
// - Has a conflict.
// - Received a error while being conflict checked.
// - Is a work-in-progress pull request.
return pr . Status != PullRequestStatusChecking && pr . Status != PullRequestStatusConflict &&
2023-10-11 06:24:07 +02:00
pr . Status != PullRequestStatusError && ! pr . IsWorkInProgress ( ctx )
2022-04-21 21:55:45 +00:00
}
2022-06-13 17:37:59 +08:00
// HasEnoughApprovals returns true if pr has enough granted approvals.
func HasEnoughApprovals ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if protectBranch . RequiredApprovals == 0 {
return true
}
return GetGrantedApprovalsCount ( ctx , protectBranch , pr ) >= protectBranch . RequiredApprovals
}
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
func GetGrantedApprovalsCount ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) int64 {
sess := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeApprove ) .
And ( "official = ?" , true ) .
And ( "dismissed = ?" , false )
2024-01-15 08:20:01 +01:00
if protectBranch . IgnoreStaleApprovals {
2022-06-13 17:37:59 +08:00
sess = sess . And ( "stale = ?" , false )
}
approvals , err := sess . Count ( new ( Review ) )
if err != nil {
log . Error ( "GetGrantedApprovalsCount: %v" , err )
return 0
}
return approvals
}
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
func MergeBlockedByRejectedReview ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if ! protectBranch . BlockOnRejectedReviews {
return false
}
rejectExist , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeReject ) .
And ( "official = ?" , true ) .
And ( "dismissed = ?" , false ) .
Exist ( new ( Review ) )
if err != nil {
log . Error ( "MergeBlockedByRejectedReview: %v" , err )
return true
}
return rejectExist
}
// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
// of from official review
func MergeBlockedByOfficialReviewRequests ( ctx context . Context , protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
if ! protectBranch . BlockOnOfficialReviewRequests {
return false
}
has , err := db . GetEngine ( ctx ) . Where ( "issue_id = ?" , pr . IssueID ) .
And ( "type = ?" , ReviewTypeRequest ) .
And ( "official = ?" , true ) .
Exist ( new ( Review ) )
if err != nil {
log . Error ( "MergeBlockedByOfficialReviewRequests: %v" , err )
return true
}
return has
}
// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
func MergeBlockedByOutdatedBranch ( protectBranch * git_model . ProtectedBranch , pr * PullRequest ) bool {
return protectBranch . BlockOnOutdatedBranch && pr . CommitsBehind > 0
}
2023-06-08 11:56:05 +03:00
func PullRequestCodeOwnersReview ( ctx context . Context , pull * Issue , pr * PullRequest ) error {
files := [ ] string { "CODEOWNERS" , "docs/CODEOWNERS" , ".gitea/CODEOWNERS" }
2023-10-11 06:24:07 +02:00
if pr . IsWorkInProgress ( ctx ) {
2023-06-08 11:56:05 +03:00
return nil
}
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
return err
}
2024-01-28 04:09:51 +08:00
repo , err := gitrepo . OpenRepository ( ctx , pr . BaseRepo )
2023-06-08 11:56:05 +03:00
if err != nil {
return err
}
defer repo . Close ( )
branch , err := repo . GetDefaultBranch ( )
if err != nil {
return err
}
commit , err := repo . GetBranchCommit ( branch )
if err != nil {
return err
}
var data string
for _ , file := range files {
if blob , err := commit . GetBlobByPath ( file ) ; err == nil {
2023-06-13 18:02:25 +09:00
data , err = blob . GetBlobContent ( setting . UI . MaxDisplayFileSize )
2023-06-08 11:56:05 +03:00
if err == nil {
break
}
}
}
rules , _ := GetCodeOwnersFromContent ( ctx , data )
changedFiles , err := repo . GetFilesChangedBetween ( git . BranchPrefix + pr . BaseBranch , pr . GetGitRefName ( ) )
if err != nil {
return err
}
uniqUsers := make ( map [ int64 ] * user_model . User )
uniqTeams := make ( map [ string ] * org_model . Team )
for _ , rule := range rules {
for _ , f := range changedFiles {
if ( rule . Rule . MatchString ( f ) && ! rule . Negative ) || ( ! rule . Rule . MatchString ( f ) && rule . Negative ) {
for _ , u := range rule . Users {
uniqUsers [ u . ID ] = u
}
for _ , t := range rule . Teams {
uniqTeams [ fmt . Sprintf ( "%d/%d" , t . OrgID , t . ID ) ] = t
}
}
}
}
for _ , u := range uniqUsers {
if u . ID != pull . Poster . ID {
2023-08-10 10:39:21 +08:00
if _ , err := AddReviewRequest ( ctx , pull , u , pull . Poster ) ; err != nil {
2023-06-08 11:56:05 +03:00
log . Warn ( "Failed add assignee user: %s to PR review: %s#%d, error: %s" , u . Name , pr . BaseRepo . Name , pr . ID , err )
return err
}
}
}
for _ , t := range uniqTeams {
2023-08-10 10:39:21 +08:00
if _ , err := AddTeamReviewRequest ( ctx , pull , t , pull . Poster ) ; err != nil {
2023-06-08 11:56:05 +03:00
log . Warn ( "Failed add assignee team: %s to PR review: %s#%d, error: %s" , t . Name , pr . BaseRepo . Name , pr . ID , err )
return err
}
}
return nil
}
// GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing
// Return warning messages on parsing errors
// We're trying to do the best we can when parsing a file.
// Invalid lines are skipped. Non-existent users and teams too.
func GetCodeOwnersFromContent ( ctx context . Context , data string ) ( [ ] * CodeOwnerRule , [ ] string ) {
if len ( data ) == 0 {
return nil , nil
}
rules := make ( [ ] * CodeOwnerRule , 0 )
lines := strings . Split ( data , "\n" )
warnings := make ( [ ] string , 0 )
for i , line := range lines {
tokens := TokenizeCodeOwnersLine ( line )
if len ( tokens ) == 0 {
continue
} else if len ( tokens ) < 2 {
warnings = append ( warnings , fmt . Sprintf ( "Line: %d: incorrect format" , i + 1 ) )
continue
}
rule , wr := ParseCodeOwnersLine ( ctx , tokens )
for _ , w := range wr {
warnings = append ( warnings , fmt . Sprintf ( "Line: %d: %s" , i + 1 , w ) )
}
if rule == nil {
continue
}
rules = append ( rules , rule )
}
return rules , warnings
}
type CodeOwnerRule struct {
Rule * regexp . Regexp
Negative bool
Users [ ] * user_model . User
Teams [ ] * org_model . Team
}
func ParseCodeOwnersLine ( ctx context . Context , tokens [ ] string ) ( * CodeOwnerRule , [ ] string ) {
var err error
rule := & CodeOwnerRule {
Users : make ( [ ] * user_model . User , 0 ) ,
Teams : make ( [ ] * org_model . Team , 0 ) ,
Negative : strings . HasPrefix ( tokens [ 0 ] , "!" ) ,
}
warnings := make ( [ ] string , 0 )
rule . Rule , err = regexp . Compile ( fmt . Sprintf ( "^%s$" , strings . TrimPrefix ( tokens [ 0 ] , "!" ) ) )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner regexp: %s" , err ) )
return nil , warnings
}
for _ , user := range tokens [ 1 : ] {
user = strings . TrimPrefix ( user , "@" )
// Only @org/team can contain slashes
if strings . Contains ( user , "/" ) {
s := strings . Split ( user , "/" )
if len ( s ) != 2 {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner group: %s" , user ) )
continue
}
orgName := s [ 0 ]
teamName := s [ 1 ]
org , err := org_model . GetOrgByName ( ctx , orgName )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner organization: %s" , user ) )
continue
}
2023-10-03 12:30:41 +02:00
teams , err := org . LoadTeams ( ctx )
2023-06-08 11:56:05 +03:00
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner team: %s" , user ) )
continue
}
for _ , team := range teams {
if team . Name == teamName {
rule . Teams = append ( rule . Teams , team )
}
}
} else {
u , err := user_model . GetUserByName ( ctx , user )
if err != nil {
warnings = append ( warnings , fmt . Sprintf ( "incorrect codeowner user: %s" , user ) )
continue
}
rule . Users = append ( rule . Users , u )
}
}
if ( len ( rule . Users ) == 0 ) && ( len ( rule . Teams ) == 0 ) {
warnings = append ( warnings , "no users/groups matched" )
return nil , warnings
}
return rule , warnings
}
func TokenizeCodeOwnersLine ( line string ) [ ] string {
if len ( line ) == 0 {
return nil
}
line = strings . TrimSpace ( line )
line = strings . ReplaceAll ( line , "\t" , " " )
tokens := make ( [ ] string , 0 )
escape := false
token := ""
for _ , char := range line {
if escape {
token += string ( char )
escape = false
} else if string ( char ) == "\\" {
escape = true
} else if string ( char ) == "#" {
break
} else if string ( char ) == " " {
if len ( token ) > 0 {
tokens = append ( tokens , token )
token = ""
}
} else {
token += string ( char )
}
}
if len ( token ) > 0 {
tokens = append ( tokens , token )
}
return tokens
}
2023-09-09 05:09:23 +08:00
// InsertPullRequests inserted pull requests
func InsertPullRequests ( ctx context . Context , prs ... * PullRequest ) error {
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
for _ , pr := range prs {
if err := insertIssue ( ctx , pr . Issue ) ; err != nil {
return err
}
pr . IssueID = pr . Issue . ID
if _ , err := sess . NoAutoTime ( ) . Insert ( pr ) ; err != nil {
return err
}
}
return committer . Commit ( )
}