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