code.gitea.io/gitea@v1.22.3/models/issues/pull.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package issues 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "regexp" 12 "strconv" 13 "strings" 14 15 "code.gitea.io/gitea/models/db" 16 git_model "code.gitea.io/gitea/models/git" 17 org_model "code.gitea.io/gitea/models/organization" 18 pull_model "code.gitea.io/gitea/models/pull" 19 repo_model "code.gitea.io/gitea/models/repo" 20 user_model "code.gitea.io/gitea/models/user" 21 "code.gitea.io/gitea/modules/git" 22 "code.gitea.io/gitea/modules/log" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/modules/timeutil" 25 "code.gitea.io/gitea/modules/util" 26 27 "xorm.io/builder" 28 ) 29 30 var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator") 31 32 // ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error. 33 type ErrPullRequestNotExist struct { 34 ID int64 35 IssueID int64 36 HeadRepoID int64 37 BaseRepoID int64 38 HeadBranch string 39 BaseBranch string 40 } 41 42 // IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist. 43 func IsErrPullRequestNotExist(err error) bool { 44 _, ok := err.(ErrPullRequestNotExist) 45 return ok 46 } 47 48 func (err ErrPullRequestNotExist) Error() string { 49 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]", 50 err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) 51 } 52 53 func (err ErrPullRequestNotExist) Unwrap() error { 54 return util.ErrNotExist 55 } 56 57 // ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error 58 type ErrPullRequestAlreadyExists struct { 59 ID int64 60 IssueID int64 61 HeadRepoID int64 62 BaseRepoID int64 63 HeadBranch string 64 BaseBranch string 65 } 66 67 // IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists. 68 func IsErrPullRequestAlreadyExists(err error) bool { 69 _, ok := err.(ErrPullRequestAlreadyExists) 70 return ok 71 } 72 73 // Error does pretty-printing :D 74 func (err ErrPullRequestAlreadyExists) Error() string { 75 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]", 76 err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) 77 } 78 79 func (err ErrPullRequestAlreadyExists) Unwrap() error { 80 return util.ErrAlreadyExist 81 } 82 83 // ErrPullWasClosed is used close a closed pull request 84 type ErrPullWasClosed struct { 85 ID int64 86 Index int64 87 } 88 89 // IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. 90 func IsErrPullWasClosed(err error) bool { 91 _, ok := err.(ErrPullWasClosed) 92 return ok 93 } 94 95 func (err ErrPullWasClosed) Error() string { 96 return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) 97 } 98 99 // PullRequestType defines pull request type 100 type PullRequestType int 101 102 // Enumerate all the pull request types 103 const ( 104 PullRequestGitea PullRequestType = iota 105 PullRequestGit 106 ) 107 108 // PullRequestStatus defines pull request status 109 type PullRequestStatus int 110 111 // Enumerate all the pull request status 112 const ( 113 PullRequestStatusConflict PullRequestStatus = iota 114 PullRequestStatusChecking 115 PullRequestStatusMergeable 116 PullRequestStatusManuallyMerged 117 PullRequestStatusError 118 PullRequestStatusEmpty 119 PullRequestStatusAncestor 120 ) 121 122 func (status PullRequestStatus) String() string { 123 switch status { 124 case PullRequestStatusConflict: 125 return "CONFLICT" 126 case PullRequestStatusChecking: 127 return "CHECKING" 128 case PullRequestStatusMergeable: 129 return "MERGEABLE" 130 case PullRequestStatusManuallyMerged: 131 return "MANUALLY_MERGED" 132 case PullRequestStatusError: 133 return "ERROR" 134 case PullRequestStatusEmpty: 135 return "EMPTY" 136 case PullRequestStatusAncestor: 137 return "ANCESTOR" 138 default: 139 return strconv.Itoa(int(status)) 140 } 141 } 142 143 // PullRequestFlow the flow of pull request 144 type PullRequestFlow int 145 146 const ( 147 // PullRequestFlowGithub github flow from head branch to base branch 148 PullRequestFlowGithub PullRequestFlow = iota 149 // PullRequestFlowAGit Agit flow pull request, head branch is not exist 150 PullRequestFlowAGit 151 ) 152 153 // PullRequest represents relation between pull request and repositories. 154 type PullRequest struct { 155 ID int64 `xorm:"pk autoincr"` 156 Type PullRequestType 157 Status PullRequestStatus 158 ConflictedFiles []string `xorm:"TEXT JSON"` 159 CommitsAhead int 160 CommitsBehind int 161 162 ChangedProtectedFiles []string `xorm:"TEXT JSON"` 163 164 IssueID int64 `xorm:"INDEX"` 165 Issue *Issue `xorm:"-"` 166 Index int64 167 RequestedReviewers []*user_model.User `xorm:"-"` 168 169 HeadRepoID int64 `xorm:"INDEX"` 170 HeadRepo *repo_model.Repository `xorm:"-"` 171 BaseRepoID int64 `xorm:"INDEX"` 172 BaseRepo *repo_model.Repository `xorm:"-"` 173 HeadBranch string 174 HeadCommitID string `xorm:"-"` 175 BaseBranch string 176 MergeBase string `xorm:"VARCHAR(64)"` 177 AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` 178 179 HasMerged bool `xorm:"INDEX"` 180 MergedCommitID string `xorm:"VARCHAR(64)"` 181 MergerID int64 `xorm:"INDEX"` 182 Merger *user_model.User `xorm:"-"` 183 MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` 184 185 isHeadRepoLoaded bool `xorm:"-"` 186 187 Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` 188 } 189 190 func init() { 191 db.RegisterModel(new(PullRequest)) 192 } 193 194 // DeletePullsByBaseRepoID deletes all pull requests by the base repository ID 195 func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error { 196 deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID}) 197 198 // Delete scheduled auto merges 199 if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). 200 Delete(&pull_model.AutoMerge{}); err != nil { 201 return err 202 } 203 204 // Delete review states 205 if _, err := db.GetEngine(ctx).In("pull_id", deleteCond). 206 Delete(&pull_model.ReviewState{}); err != nil { 207 return err 208 } 209 210 _, err := db.DeleteByBean(ctx, &PullRequest{BaseRepoID: repoID}) 211 return err 212 } 213 214 func (pr *PullRequest) String() string { 215 if pr == nil { 216 return "<PullRequest nil>" 217 } 218 219 s := new(strings.Builder) 220 fmt.Fprintf(s, "<PullRequest [%d]", pr.ID) 221 if pr.BaseRepo != nil { 222 fmt.Fprintf(s, "%s#%d[%s...", pr.BaseRepo.FullName(), pr.Index, pr.BaseBranch) 223 } else { 224 fmt.Fprintf(s, "Repo[%d]#%d[%s...", pr.BaseRepoID, pr.Index, pr.BaseBranch) 225 } 226 if pr.HeadRepoID == pr.BaseRepoID { 227 fmt.Fprintf(s, "%s]", pr.HeadBranch) 228 } else if pr.HeadRepo != nil { 229 fmt.Fprintf(s, "%s:%s]", pr.HeadRepo.FullName(), pr.HeadBranch) 230 } else { 231 fmt.Fprintf(s, "Repo[%d]:%s]", pr.HeadRepoID, pr.HeadBranch) 232 } 233 s.WriteByte('>') 234 return s.String() 235 } 236 237 // MustHeadUserName returns the HeadRepo's username if failed return blank 238 func (pr *PullRequest) MustHeadUserName(ctx context.Context) string { 239 if err := pr.LoadHeadRepo(ctx); err != nil { 240 if !repo_model.IsErrRepoNotExist(err) { 241 log.Error("LoadHeadRepo: %v", err) 242 } else { 243 log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err) 244 } 245 return "" 246 } 247 if pr.HeadRepo == nil { 248 return "" 249 } 250 return pr.HeadRepo.OwnerName 251 } 252 253 // LoadAttributes loads pull request attributes from database 254 // Note: don't try to get Issue because will end up recursive querying. 255 func (pr *PullRequest) LoadAttributes(ctx context.Context) (err error) { 256 if pr.HasMerged && pr.Merger == nil { 257 pr.Merger, err = user_model.GetUserByID(ctx, pr.MergerID) 258 if user_model.IsErrUserNotExist(err) { 259 pr.MergerID = user_model.GhostUserID 260 pr.Merger = user_model.NewGhostUser() 261 } else if err != nil { 262 return fmt.Errorf("getUserByID [%d]: %w", pr.MergerID, err) 263 } 264 } 265 266 return nil 267 } 268 269 // LoadHeadRepo loads the head repository, pr.HeadRepo will remain nil if it does not exist 270 // and thus ErrRepoNotExist will never be returned 271 func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) { 272 if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 { 273 if pr.HeadRepoID == pr.BaseRepoID { 274 if pr.BaseRepo != nil { 275 pr.HeadRepo = pr.BaseRepo 276 return nil 277 } else if pr.Issue != nil && pr.Issue.Repo != nil { 278 pr.HeadRepo = pr.Issue.Repo 279 return nil 280 } 281 } 282 283 pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID) 284 if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work 285 return fmt.Errorf("pr[%d].LoadHeadRepo[%d]: %w", pr.ID, pr.HeadRepoID, err) 286 } 287 pr.isHeadRepoLoaded = true 288 } 289 return nil 290 } 291 292 // LoadRequestedReviewers loads the requested reviewers. 293 func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { 294 if len(pr.RequestedReviewers) > 0 { 295 return nil 296 } 297 298 reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) 299 if err != nil { 300 return err 301 } 302 303 if err = reviews.LoadReviewers(ctx); err != nil { 304 return err 305 } 306 for _, review := range reviews { 307 pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer) 308 } 309 310 return nil 311 } 312 313 // LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned. 314 func (pr *PullRequest) LoadBaseRepo(ctx context.Context) (err error) { 315 if pr.BaseRepo != nil { 316 return nil 317 } 318 319 if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { 320 pr.BaseRepo = pr.HeadRepo 321 return nil 322 } 323 324 if pr.Issue != nil && pr.Issue.Repo != nil { 325 pr.BaseRepo = pr.Issue.Repo 326 return nil 327 } 328 329 pr.BaseRepo, err = repo_model.GetRepositoryByID(ctx, pr.BaseRepoID) 330 if err != nil { 331 return fmt.Errorf("pr[%d].LoadBaseRepo[%d]: %w", pr.ID, pr.BaseRepoID, err) 332 } 333 return nil 334 } 335 336 // LoadIssue loads issue information from database 337 func (pr *PullRequest) LoadIssue(ctx context.Context) (err error) { 338 if pr.Issue != nil { 339 return nil 340 } 341 342 pr.Issue, err = GetIssueByID(ctx, pr.IssueID) 343 if err == nil { 344 pr.Issue.PullRequest = pr 345 } 346 return err 347 } 348 349 // ReviewCount represents a count of Reviews 350 type ReviewCount struct { 351 IssueID int64 352 Type ReviewType 353 Count int64 354 } 355 356 // GetApprovalCounts returns the approval counts by type 357 // FIXME: Only returns official counts due to double counting of non-official counts 358 func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) { 359 rCounts := make([]*ReviewCount, 0, 6) 360 sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID) 361 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) 362 } 363 364 // GetApprovers returns the approvers of the pull request 365 func (pr *PullRequest) GetApprovers(ctx context.Context) string { 366 stringBuilder := strings.Builder{} 367 if err := pr.getReviewedByLines(ctx, &stringBuilder); err != nil { 368 log.Error("Unable to getReviewedByLines: Error: %v", err) 369 return "" 370 } 371 372 return stringBuilder.String() 373 } 374 375 func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer) error { 376 maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers 377 378 if maxReviewers == 0 { 379 return nil 380 } 381 382 ctx, committer, err := db.TxContext(ctx) 383 if err != nil { 384 return err 385 } 386 defer committer.Close() 387 388 // Note: This doesn't page as we only expect a very limited number of reviews 389 reviews, err := FindLatestReviews(ctx, FindReviewOptions{ 390 Types: []ReviewType{ReviewTypeApprove}, 391 IssueID: pr.IssueID, 392 OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly, 393 }) 394 if err != nil { 395 log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err) 396 return err 397 } 398 399 reviewersWritten := 0 400 401 for _, review := range reviews { 402 if maxReviewers > 0 && reviewersWritten > maxReviewers { 403 break 404 } 405 406 if err := review.LoadReviewer(ctx); err != nil && !user_model.IsErrUserNotExist(err) { 407 log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err) 408 return err 409 } else if review.Reviewer == nil { 410 continue 411 } 412 if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil { 413 return err 414 } 415 if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil { 416 return err 417 } 418 if _, err := writer.Write([]byte{'\n'}); err != nil { 419 return err 420 } 421 reviewersWritten++ 422 } 423 return committer.Commit() 424 } 425 426 // GetGitRefName returns git ref for hidden pull request branch 427 func (pr *PullRequest) GetGitRefName() string { 428 return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) 429 } 430 431 func (pr *PullRequest) GetGitHeadBranchRefName() string { 432 return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch) 433 } 434 435 // GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) 436 func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int { 437 opts := FindCommentsOptions{ 438 Type: CommentTypeReview, 439 IssueID: pr.IssueID, 440 } 441 conds := opts.ToConds() 442 443 count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment)) 444 if err != nil { 445 return 0 446 } 447 return int(count) 448 } 449 450 // IsChecking returns true if this pull request is still checking conflict. 451 func (pr *PullRequest) IsChecking() bool { 452 return pr.Status == PullRequestStatusChecking 453 } 454 455 // CanAutoMerge returns true if this pull request can be merged automatically. 456 func (pr *PullRequest) CanAutoMerge() bool { 457 return pr.Status == PullRequestStatusMergeable 458 } 459 460 // IsEmpty returns true if this pull request is empty. 461 func (pr *PullRequest) IsEmpty() bool { 462 return pr.Status == PullRequestStatusEmpty 463 } 464 465 // IsAncestor returns true if the Head Commit of this PR is an ancestor of the Base Commit 466 func (pr *PullRequest) IsAncestor() bool { 467 return pr.Status == PullRequestStatusAncestor 468 } 469 470 // IsFromFork return true if this PR is from a fork. 471 func (pr *PullRequest) IsFromFork() bool { 472 return pr.HeadRepoID != pr.BaseRepoID 473 } 474 475 // SetMerged sets a pull request to merged and closes the corresponding issue 476 func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { 477 if pr.HasMerged { 478 return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) 479 } 480 if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { 481 return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) 482 } 483 484 pr.HasMerged = true 485 sess := db.GetEngine(ctx) 486 487 if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { 488 return false, err 489 } 490 491 if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { 492 return false, err 493 } 494 495 pr.Issue = nil 496 if err := pr.LoadIssue(ctx); err != nil { 497 return false, err 498 } 499 500 if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil { 501 return false, err 502 } else if tmpPr.HasMerged { 503 if pr.Issue.IsClosed { 504 return false, nil 505 } 506 return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) 507 } else if pr.Issue.IsClosed { 508 return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) 509 } 510 511 if err := pr.Issue.LoadRepo(ctx); err != nil { 512 return false, err 513 } 514 515 if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { 516 return false, err 517 } 518 519 if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { 520 return false, fmt.Errorf("Issue.changeStatus: %w", err) 521 } 522 523 // reset the conflicted files as there cannot be any if we're merged 524 pr.ConflictedFiles = []string{} 525 526 // 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. 527 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 { 528 return false, fmt.Errorf("Failed to update pr[%d]: %w", pr.ID, err) 529 } 530 531 return true, nil 532 } 533 534 // NewPullRequest creates new pull request with labels for repository. 535 func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { 536 ctx, committer, err := db.TxContext(ctx) 537 if err != nil { 538 return err 539 } 540 defer committer.Close() 541 542 idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) 543 if err != nil { 544 return fmt.Errorf("generate pull request index failed: %w", err) 545 } 546 547 issue.Index = idx 548 549 if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ 550 Repo: repo, 551 Issue: issue, 552 LabelIDs: labelIDs, 553 Attachments: uuids, 554 IsPull: true, 555 }); err != nil { 556 if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { 557 return err 558 } 559 return fmt.Errorf("newIssue: %w", err) 560 } 561 562 pr.Index = issue.Index 563 pr.BaseRepo = repo 564 pr.IssueID = issue.ID 565 if err = db.Insert(ctx, pr); err != nil { 566 return fmt.Errorf("insert pull repo: %w", err) 567 } 568 569 if err = committer.Commit(); err != nil { 570 return fmt.Errorf("Commit: %w", err) 571 } 572 573 return nil 574 } 575 576 // ErrUserMustCollaborator represents an error that the user must be a collaborator to a given repo. 577 type ErrUserMustCollaborator struct { 578 UserID int64 579 RepoName string 580 } 581 582 // GetUnmergedPullRequest returns a pull request that is open and has not been merged 583 // by given head/base and repo/branch. 584 func GetUnmergedPullRequest(ctx context.Context, headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) { 585 pr := new(PullRequest) 586 has, err := db.GetEngine(ctx). 587 Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?", 588 headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false). 589 Join("INNER", "issue", "issue.id=pull_request.issue_id"). 590 Get(pr) 591 if err != nil { 592 return nil, err 593 } else if !has { 594 return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} 595 } 596 597 return pr, nil 598 } 599 600 // GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) 601 // by given head information (repo and branch). 602 func GetLatestPullRequestByHeadInfo(ctx context.Context, repoID int64, branch string) (*PullRequest, error) { 603 pr := new(PullRequest) 604 has, err := db.GetEngine(ctx). 605 Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub). 606 OrderBy("id DESC"). 607 Get(pr) 608 if !has { 609 return nil, err 610 } 611 return pr, err 612 } 613 614 // GetPullRequestByIndex returns a pull request by the given index 615 func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullRequest, error) { 616 if index < 1 { 617 return nil, ErrPullRequestNotExist{} 618 } 619 pr := &PullRequest{ 620 BaseRepoID: repoID, 621 Index: index, 622 } 623 624 has, err := db.GetEngine(ctx).Get(pr) 625 if err != nil { 626 return nil, err 627 } else if !has { 628 return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} 629 } 630 631 if err = pr.LoadAttributes(ctx); err != nil { 632 return nil, err 633 } 634 if err = pr.LoadIssue(ctx); err != nil { 635 return nil, err 636 } 637 638 return pr, nil 639 } 640 641 // GetPullRequestByID returns a pull request by given ID. 642 func GetPullRequestByID(ctx context.Context, id int64) (*PullRequest, error) { 643 pr := new(PullRequest) 644 has, err := db.GetEngine(ctx).ID(id).Get(pr) 645 if err != nil { 646 return nil, err 647 } else if !has { 648 return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""} 649 } 650 return pr, pr.LoadAttributes(ctx) 651 } 652 653 // GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID. 654 func GetPullRequestByIssueIDWithNoAttributes(ctx context.Context, issueID int64) (*PullRequest, error) { 655 var pr PullRequest 656 has, err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Get(&pr) 657 if err != nil { 658 return nil, err 659 } 660 if !has { 661 return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} 662 } 663 return &pr, nil 664 } 665 666 // GetPullRequestByIssueID returns pull request by given issue ID. 667 func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) { 668 pr, exist, err := db.Get[PullRequest](ctx, builder.Eq{"issue_id": issueID}) 669 if err != nil { 670 return nil, err 671 } else if !exist { 672 return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} 673 } 674 return pr, pr.LoadAttributes(ctx) 675 } 676 677 // GetPullRequestsByBaseHeadInfo returns the pull request by given base and head 678 func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) { 679 pr := &PullRequest{} 680 sess := db.GetEngine(ctx). 681 Join("INNER", "issue", "issue.id = pull_request.issue_id"). 682 Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head) 683 has, err := sess.Get(pr) 684 if err != nil { 685 return nil, err 686 } 687 if !has { 688 return nil, ErrPullRequestNotExist{ 689 HeadRepoID: headID, 690 BaseRepoID: baseID, 691 HeadBranch: head, 692 BaseBranch: base, 693 } 694 } 695 696 if err = pr.LoadAttributes(ctx); err != nil { 697 return nil, err 698 } 699 if err = pr.LoadIssue(ctx); err != nil { 700 return nil, err 701 } 702 703 return pr, nil 704 } 705 706 // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request 707 // By poster id. 708 func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) { 709 pulls := make([]*PullRequest, 0, 10) 710 711 err := db.GetEngine(ctx). 712 Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?", 713 false, PullRequestFlowAGit, false, uid). 714 Join("INNER", "issue", "issue.id=pull_request.issue_id"). 715 Find(&pulls) 716 717 return pulls, err 718 } 719 720 // Update updates all fields of pull request. 721 func (pr *PullRequest) Update(ctx context.Context) error { 722 _, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr) 723 return err 724 } 725 726 // UpdateCols updates specific fields of pull request. 727 func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error { 728 _, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr) 729 return err 730 } 731 732 // UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged 733 func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) error { 734 _, err := db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) 735 return err 736 } 737 738 // IsWorkInProgress determine if the Pull Request is a Work In Progress by its title 739 // Issue must be set before this method can be called. 740 func (pr *PullRequest) IsWorkInProgress(ctx context.Context) bool { 741 if err := pr.LoadIssue(ctx); err != nil { 742 log.Error("LoadIssue: %v", err) 743 return false 744 } 745 return HasWorkInProgressPrefix(pr.Issue.Title) 746 } 747 748 // HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix 749 func HasWorkInProgressPrefix(title string) bool { 750 for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { 751 if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) { 752 return true 753 } 754 } 755 return false 756 } 757 758 // IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. 759 func (pr *PullRequest) IsFilesConflicted() bool { 760 return len(pr.ConflictedFiles) > 0 761 } 762 763 // GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress. 764 // It returns an empty string when none were found 765 func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string { 766 if err := pr.LoadIssue(ctx); err != nil { 767 log.Error("LoadIssue: %v", err) 768 return "" 769 } 770 771 for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { 772 if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), strings.ToUpper(prefix)) { 773 return pr.Issue.Title[0:len(prefix)] 774 } 775 } 776 return "" 777 } 778 779 // UpdateCommitDivergence update Divergence of a pull request 780 func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error { 781 if pr.ID == 0 { 782 return fmt.Errorf("pull ID is 0") 783 } 784 pr.CommitsAhead = ahead 785 pr.CommitsBehind = behind 786 _, err := db.GetEngine(ctx).ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr) 787 return err 788 } 789 790 // IsSameRepo returns true if base repo and head repo is the same 791 func (pr *PullRequest) IsSameRepo() bool { 792 return pr.BaseRepoID == pr.HeadRepoID 793 } 794 795 // GetBaseBranchLink returns the relative URL of the base branch 796 func (pr *PullRequest) GetBaseBranchLink(ctx context.Context) string { 797 if err := pr.LoadBaseRepo(ctx); err != nil { 798 log.Error("LoadBaseRepo: %v", err) 799 return "" 800 } 801 if pr.BaseRepo == nil { 802 return "" 803 } 804 return pr.BaseRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) 805 } 806 807 // GetHeadBranchLink returns the relative URL of the head branch 808 func (pr *PullRequest) GetHeadBranchLink(ctx context.Context) string { 809 if pr.Flow == PullRequestFlowAGit { 810 return "" 811 } 812 813 if err := pr.LoadHeadRepo(ctx); err != nil { 814 log.Error("LoadHeadRepo: %v", err) 815 return "" 816 } 817 if pr.HeadRepo == nil { 818 return "" 819 } 820 return pr.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) 821 } 822 823 // UpdateAllowEdits update if PR can be edited from maintainers 824 func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error { 825 if _, err := db.GetEngine(ctx).ID(pr.ID).Cols("allow_maintainer_edit").Update(pr); err != nil { 826 return err 827 } 828 return nil 829 } 830 831 // Mergeable returns if the pullrequest is mergeable. 832 func (pr *PullRequest) Mergeable(ctx context.Context) bool { 833 // If a pull request isn't mergeable if it's: 834 // - Being conflict checked. 835 // - Has a conflict. 836 // - Received a error while being conflict checked. 837 // - Is a work-in-progress pull request. 838 return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict && 839 pr.Status != PullRequestStatusError && !pr.IsWorkInProgress(ctx) 840 } 841 842 // HasEnoughApprovals returns true if pr has enough granted approvals. 843 func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { 844 if protectBranch.RequiredApprovals == 0 { 845 return true 846 } 847 return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals 848 } 849 850 // GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist. 851 func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 { 852 sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). 853 And("type = ?", ReviewTypeApprove). 854 And("official = ?", true). 855 And("dismissed = ?", false) 856 if protectBranch.IgnoreStaleApprovals { 857 sess = sess.And("stale = ?", false) 858 } 859 approvals, err := sess.Count(new(Review)) 860 if err != nil { 861 log.Error("GetGrantedApprovalsCount: %v", err) 862 return 0 863 } 864 865 return approvals 866 } 867 868 // MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews 869 func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { 870 if !protectBranch.BlockOnRejectedReviews { 871 return false 872 } 873 rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). 874 And("type = ?", ReviewTypeReject). 875 And("official = ?", true). 876 And("dismissed = ?", false). 877 Exist(new(Review)) 878 if err != nil { 879 log.Error("MergeBlockedByRejectedReview: %v", err) 880 return true 881 } 882 883 return rejectExist 884 } 885 886 // MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer 887 // of from official review 888 func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { 889 if !protectBranch.BlockOnOfficialReviewRequests { 890 return false 891 } 892 has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID). 893 And("type = ?", ReviewTypeRequest). 894 And("official = ?", true). 895 Exist(new(Review)) 896 if err != nil { 897 log.Error("MergeBlockedByOfficialReviewRequests: %v", err) 898 return true 899 } 900 901 return has 902 } 903 904 // MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch 905 func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { 906 return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 907 } 908 909 // GetCodeOwnersFromContent returns the code owners configuration 910 // Return empty slice if files missing 911 // Return warning messages on parsing errors 912 // We're trying to do the best we can when parsing a file. 913 // Invalid lines are skipped. Non-existent users and teams too. 914 func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) { 915 if len(data) == 0 { 916 return nil, nil 917 } 918 919 rules := make([]*CodeOwnerRule, 0) 920 lines := strings.Split(data, "\n") 921 warnings := make([]string, 0) 922 923 for i, line := range lines { 924 tokens := TokenizeCodeOwnersLine(line) 925 if len(tokens) == 0 { 926 continue 927 } else if len(tokens) < 2 { 928 warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) 929 continue 930 } 931 rule, wr := ParseCodeOwnersLine(ctx, tokens) 932 for _, w := range wr { 933 warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) 934 } 935 if rule == nil { 936 continue 937 } 938 939 rules = append(rules, rule) 940 } 941 942 return rules, warnings 943 } 944 945 type CodeOwnerRule struct { 946 Rule *regexp.Regexp 947 Negative bool 948 Users []*user_model.User 949 Teams []*org_model.Team 950 } 951 952 func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { 953 var err error 954 rule := &CodeOwnerRule{ 955 Users: make([]*user_model.User, 0), 956 Teams: make([]*org_model.Team, 0), 957 Negative: strings.HasPrefix(tokens[0], "!"), 958 } 959 960 warnings := make([]string, 0) 961 962 rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) 963 if err != nil { 964 warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) 965 return nil, warnings 966 } 967 968 for _, user := range tokens[1:] { 969 user = strings.TrimPrefix(user, "@") 970 971 // Only @org/team can contain slashes 972 if strings.Contains(user, "/") { 973 s := strings.Split(user, "/") 974 if len(s) != 2 { 975 warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user)) 976 continue 977 } 978 orgName := s[0] 979 teamName := s[1] 980 981 org, err := org_model.GetOrgByName(ctx, orgName) 982 if err != nil { 983 warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user)) 984 continue 985 } 986 teams, err := org.LoadTeams(ctx) 987 if err != nil { 988 warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user)) 989 continue 990 } 991 992 for _, team := range teams { 993 if team.Name == teamName { 994 rule.Teams = append(rule.Teams, team) 995 } 996 } 997 } else { 998 u, err := user_model.GetUserByName(ctx, user) 999 if err != nil { 1000 warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user)) 1001 continue 1002 } 1003 rule.Users = append(rule.Users, u) 1004 } 1005 } 1006 1007 if (len(rule.Users) == 0) && (len(rule.Teams) == 0) { 1008 warnings = append(warnings, "no users/groups matched") 1009 return nil, warnings 1010 } 1011 1012 return rule, warnings 1013 } 1014 1015 func TokenizeCodeOwnersLine(line string) []string { 1016 if len(line) == 0 { 1017 return nil 1018 } 1019 1020 line = strings.TrimSpace(line) 1021 line = strings.ReplaceAll(line, "\t", " ") 1022 1023 tokens := make([]string, 0) 1024 1025 escape := false 1026 token := "" 1027 for _, char := range line { 1028 if escape { 1029 token += string(char) 1030 escape = false 1031 } else if string(char) == "\\" { 1032 escape = true 1033 } else if string(char) == "#" { 1034 break 1035 } else if string(char) == " " { 1036 if len(token) > 0 { 1037 tokens = append(tokens, token) 1038 token = "" 1039 } 1040 } else { 1041 token += string(char) 1042 } 1043 } 1044 1045 if len(token) > 0 { 1046 tokens = append(tokens, token) 1047 } 1048 1049 return tokens 1050 } 1051 1052 // InsertPullRequests inserted pull requests 1053 func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { 1054 ctx, committer, err := db.TxContext(ctx) 1055 if err != nil { 1056 return err 1057 } 1058 defer committer.Close() 1059 sess := db.GetEngine(ctx) 1060 for _, pr := range prs { 1061 if err := insertIssue(ctx, pr.Issue); err != nil { 1062 return err 1063 } 1064 pr.IssueID = pr.Issue.ID 1065 if _, err := sess.NoAutoTime().Insert(pr); err != nil { 1066 return err 1067 } 1068 } 1069 return committer.Commit() 1070 } 1071 1072 // GetPullRequestByMergedCommit returns a merged pull request by the given commit 1073 func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) { 1074 pr := new(PullRequest) 1075 has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr) 1076 if err != nil { 1077 return nil, err 1078 } else if !has { 1079 return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} 1080 } 1081 1082 if err = pr.LoadAttributes(ctx); err != nil { 1083 return nil, err 1084 } 1085 if err = pr.LoadIssue(ctx); err != nil { 1086 return nil, err 1087 } 1088 1089 return pr, nil 1090 }