1// Copyright 2015 The Gogs Authors. All rights reserved. 2// Copyright 2019 The Gitea Authors. All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package models 7 8import ( 9 "context" 10 "fmt" 11 "io" 12 "strings" 13 14 "code.gitea.io/gitea/models/db" 15 repo_model "code.gitea.io/gitea/models/repo" 16 "code.gitea.io/gitea/models/unit" 17 user_model "code.gitea.io/gitea/models/user" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/setting" 21 "code.gitea.io/gitea/modules/timeutil" 22 "code.gitea.io/gitea/modules/util" 23) 24 25// PullRequestType defines pull request type 26type PullRequestType int 27 28// Enumerate all the pull request types 29const ( 30 PullRequestGitea PullRequestType = iota 31 PullRequestGit 32) 33 34// PullRequestStatus defines pull request status 35type PullRequestStatus int 36 37// Enumerate all the pull request status 38const ( 39 PullRequestStatusConflict PullRequestStatus = iota 40 PullRequestStatusChecking 41 PullRequestStatusMergeable 42 PullRequestStatusManuallyMerged 43 PullRequestStatusError 44 PullRequestStatusEmpty 45) 46 47// PullRequestFlow the flow of pull request 48type PullRequestFlow int 49 50const ( 51 // PullRequestFlowGithub github flow from head branch to base branch 52 PullRequestFlowGithub PullRequestFlow = iota 53 // PullRequestFlowAGit Agit flow pull request, head branch is not exist 54 PullRequestFlowAGit 55) 56 57// PullRequest represents relation between pull request and repositories. 58type PullRequest struct { 59 ID int64 `xorm:"pk autoincr"` 60 Type PullRequestType 61 Status PullRequestStatus 62 ConflictedFiles []string `xorm:"TEXT JSON"` 63 CommitsAhead int 64 CommitsBehind int 65 66 ChangedProtectedFiles []string `xorm:"TEXT JSON"` 67 68 IssueID int64 `xorm:"INDEX"` 69 Issue *Issue `xorm:"-"` 70 Index int64 71 72 HeadRepoID int64 `xorm:"INDEX"` 73 HeadRepo *repo_model.Repository `xorm:"-"` 74 BaseRepoID int64 `xorm:"INDEX"` 75 BaseRepo *repo_model.Repository `xorm:"-"` 76 HeadBranch string 77 HeadCommitID string `xorm:"-"` 78 BaseBranch string 79 ProtectedBranch *ProtectedBranch `xorm:"-"` 80 MergeBase string `xorm:"VARCHAR(40)"` 81 82 HasMerged bool `xorm:"INDEX"` 83 MergedCommitID string `xorm:"VARCHAR(40)"` 84 MergerID int64 `xorm:"INDEX"` 85 Merger *user_model.User `xorm:"-"` 86 MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` 87 88 isHeadRepoLoaded bool `xorm:"-"` 89 90 Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` 91} 92 93func init() { 94 db.RegisterModel(new(PullRequest)) 95} 96 97// MustHeadUserName returns the HeadRepo's username if failed return blank 98func (pr *PullRequest) MustHeadUserName() string { 99 if err := pr.LoadHeadRepo(); err != nil { 100 if !repo_model.IsErrRepoNotExist(err) { 101 log.Error("LoadHeadRepo: %v", err) 102 } else { 103 log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err) 104 } 105 return "" 106 } 107 if pr.HeadRepo == nil { 108 return "" 109 } 110 return pr.HeadRepo.OwnerName 111} 112 113// Note: don't try to get Issue because will end up recursive querying. 114func (pr *PullRequest) loadAttributes(e db.Engine) (err error) { 115 if pr.HasMerged && pr.Merger == nil { 116 pr.Merger, err = user_model.GetUserByIDEngine(e, pr.MergerID) 117 if user_model.IsErrUserNotExist(err) { 118 pr.MergerID = -1 119 pr.Merger = user_model.NewGhostUser() 120 } else if err != nil { 121 return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err) 122 } 123 } 124 125 return nil 126} 127 128// LoadAttributes loads pull request attributes from database 129func (pr *PullRequest) LoadAttributes() error { 130 return pr.loadAttributes(db.GetEngine(db.DefaultContext)) 131} 132 133func (pr *PullRequest) loadHeadRepo(ctx context.Context) (err error) { 134 if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 { 135 if pr.HeadRepoID == pr.BaseRepoID { 136 if pr.BaseRepo != nil { 137 pr.HeadRepo = pr.BaseRepo 138 return nil 139 } else if pr.Issue != nil && pr.Issue.Repo != nil { 140 pr.HeadRepo = pr.Issue.Repo 141 return nil 142 } 143 } 144 145 pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID) 146 if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work 147 return fmt.Errorf("getRepositoryByID(head): %v", err) 148 } 149 pr.isHeadRepoLoaded = true 150 } 151 return nil 152} 153 154// LoadHeadRepo loads the head repository 155func (pr *PullRequest) LoadHeadRepo() error { 156 return pr.loadHeadRepo(db.DefaultContext) 157} 158 159// LoadBaseRepo loads the target repository 160func (pr *PullRequest) LoadBaseRepo() error { 161 return pr.loadBaseRepo(db.DefaultContext) 162} 163 164func (pr *PullRequest) loadBaseRepo(ctx context.Context) (err error) { 165 if pr.BaseRepo != nil { 166 return nil 167 } 168 169 if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { 170 pr.BaseRepo = pr.HeadRepo 171 return nil 172 } 173 174 if pr.Issue != nil && pr.Issue.Repo != nil { 175 pr.BaseRepo = pr.Issue.Repo 176 return nil 177 } 178 179 pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) 180 if err != nil { 181 return fmt.Errorf("repo_model.GetRepositoryByID(base): %v", err) 182 } 183 return nil 184} 185 186// LoadIssue loads issue information from database 187func (pr *PullRequest) LoadIssue() (err error) { 188 return pr.loadIssue(db.GetEngine(db.DefaultContext)) 189} 190 191func (pr *PullRequest) loadIssue(e db.Engine) (err error) { 192 if pr.Issue != nil { 193 return nil 194 } 195 196 pr.Issue, err = getIssueByID(e, pr.IssueID) 197 if err == nil { 198 pr.Issue.PullRequest = pr 199 } 200 return err 201} 202 203// LoadProtectedBranch loads the protected branch of the base branch 204func (pr *PullRequest) LoadProtectedBranch() (err error) { 205 return pr.loadProtectedBranch(db.DefaultContext) 206} 207 208func (pr *PullRequest) loadProtectedBranch(ctx context.Context) (err error) { 209 if pr.ProtectedBranch == nil { 210 if pr.BaseRepo == nil { 211 if pr.BaseRepoID == 0 { 212 return nil 213 } 214 pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) 215 if err != nil { 216 return 217 } 218 } 219 pr.ProtectedBranch, err = getProtectedBranchBy(db.GetEngine(ctx), pr.BaseRepo.ID, pr.BaseBranch) 220 } 221 return 222} 223 224// GetDefaultMergeMessage returns default message used when merging pull request 225func (pr *PullRequest) GetDefaultMergeMessage() string { 226 if pr.HeadRepo == nil { 227 var err error 228 pr.HeadRepo, err = repo_model.GetRepositoryByID(pr.HeadRepoID) 229 if err != nil { 230 log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err) 231 return "" 232 } 233 } 234 if err := pr.LoadIssue(); err != nil { 235 log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err) 236 return "" 237 } 238 if err := pr.LoadBaseRepo(); err != nil { 239 log.Error("LoadBaseRepo: %v", err) 240 return "" 241 } 242 243 issueReference := "#" 244 if pr.BaseRepo.UnitEnabled(unit.TypeExternalTracker) { 245 issueReference = "!" 246 } 247 248 if pr.BaseRepoID == pr.HeadRepoID { 249 return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch) 250 } 251 252 return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch) 253} 254 255// ReviewCount represents a count of Reviews 256type ReviewCount struct { 257 IssueID int64 258 Type ReviewType 259 Count int64 260} 261 262// GetApprovalCounts returns the approval counts by type 263// FIXME: Only returns official counts due to double counting of non-official counts 264func (pr *PullRequest) GetApprovalCounts() ([]*ReviewCount, error) { 265 return pr.getApprovalCounts(db.GetEngine(db.DefaultContext)) 266} 267 268func (pr *PullRequest) getApprovalCounts(e db.Engine) ([]*ReviewCount, error) { 269 rCounts := make([]*ReviewCount, 0, 6) 270 sess := e.Where("issue_id = ?", pr.IssueID) 271 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) 272} 273 274// GetApprovers returns the approvers of the pull request 275func (pr *PullRequest) GetApprovers() string { 276 stringBuilder := strings.Builder{} 277 if err := pr.getReviewedByLines(&stringBuilder); err != nil { 278 log.Error("Unable to getReviewedByLines: Error: %v", err) 279 return "" 280 } 281 282 return stringBuilder.String() 283} 284 285func (pr *PullRequest) getReviewedByLines(writer io.Writer) error { 286 maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers 287 288 if maxReviewers == 0 { 289 return nil 290 } 291 292 ctx, committer, err := db.TxContext() 293 if err != nil { 294 return err 295 } 296 defer committer.Close() 297 sess := db.GetEngine(ctx) 298 299 // Note: This doesn't page as we only expect a very limited number of reviews 300 reviews, err := findReviews(sess, FindReviewOptions{ 301 Type: ReviewTypeApprove, 302 IssueID: pr.IssueID, 303 OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly, 304 }) 305 if err != nil { 306 log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err) 307 return err 308 } 309 310 reviewersWritten := 0 311 312 for _, review := range reviews { 313 if maxReviewers > 0 && reviewersWritten > maxReviewers { 314 break 315 } 316 317 if err := review.loadReviewer(sess); err != nil && !user_model.IsErrUserNotExist(err) { 318 log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err) 319 return err 320 } else if review.Reviewer == nil { 321 continue 322 } 323 if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil { 324 return err 325 } 326 if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil { 327 return err 328 } 329 if _, err := writer.Write([]byte{'\n'}); err != nil { 330 return err 331 } 332 reviewersWritten++ 333 } 334 return committer.Commit() 335} 336 337// GetDefaultSquashMessage returns default message used when squash and merging pull request 338func (pr *PullRequest) GetDefaultSquashMessage() string { 339 if err := pr.LoadIssue(); err != nil { 340 log.Error("LoadIssue: %v", err) 341 return "" 342 } 343 if err := pr.LoadBaseRepo(); err != nil { 344 log.Error("LoadBaseRepo: %v", err) 345 return "" 346 } 347 if pr.BaseRepo.UnitEnabled(unit.TypeExternalTracker) { 348 return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index) 349 } 350 return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index) 351} 352 353// GetGitRefName returns git ref for hidden pull request branch 354func (pr *PullRequest) GetGitRefName() string { 355 return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) 356} 357 358// IsChecking returns true if this pull request is still checking conflict. 359func (pr *PullRequest) IsChecking() bool { 360 return pr.Status == PullRequestStatusChecking 361} 362 363// CanAutoMerge returns true if this pull request can be merged automatically. 364func (pr *PullRequest) CanAutoMerge() bool { 365 return pr.Status == PullRequestStatusMergeable 366} 367 368// IsEmpty returns true if this pull request is empty. 369func (pr *PullRequest) IsEmpty() bool { 370 return pr.Status == PullRequestStatusEmpty 371} 372 373// SetMerged sets a pull request to merged and closes the corresponding issue 374func (pr *PullRequest) SetMerged() (bool, error) { 375 if pr.HasMerged { 376 return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) 377 } 378 if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { 379 return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) 380 } 381 382 pr.HasMerged = true 383 384 ctx, committer, err := db.TxContext() 385 if err != nil { 386 return false, err 387 } 388 defer committer.Close() 389 sess := db.GetEngine(ctx) 390 391 if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { 392 return false, err 393 } 394 395 if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { 396 return false, err 397 } 398 399 pr.Issue = nil 400 if err := pr.loadIssue(sess); err != nil { 401 return false, err 402 } 403 404 if tmpPr, err := getPullRequestByID(sess, pr.ID); err != nil { 405 return false, err 406 } else if tmpPr.HasMerged { 407 if pr.Issue.IsClosed { 408 return false, nil 409 } 410 return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) 411 } else if pr.Issue.IsClosed { 412 return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) 413 } 414 415 if err := pr.Issue.loadRepo(ctx); err != nil { 416 return false, err 417 } 418 419 if err := pr.Issue.Repo.GetOwner(ctx); err != nil { 420 return false, err 421 } 422 423 if _, err := pr.Issue.changeStatus(ctx, pr.Merger, true, true); err != nil { 424 return false, fmt.Errorf("Issue.changeStatus: %v", err) 425 } 426 427 // 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. 428 if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix").Update(pr); err != nil { 429 return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err) 430 } 431 432 if err := committer.Commit(); err != nil { 433 return false, fmt.Errorf("Commit: %v", err) 434 } 435 return true, nil 436} 437 438// NewPullRequest creates new pull request with labels for repository. 439func NewPullRequest(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { 440 idx, err := db.GetNextResourceIndex("issue_index", repo.ID) 441 if err != nil { 442 return fmt.Errorf("generate pull request index failed: %v", err) 443 } 444 445 issue.Index = idx 446 447 ctx, committer, err := db.TxContext() 448 if err != nil { 449 return err 450 } 451 defer committer.Close() 452 453 if err = newIssue(ctx, issue.Poster, NewIssueOptions{ 454 Repo: repo, 455 Issue: issue, 456 LabelIDs: labelIDs, 457 Attachments: uuids, 458 IsPull: true, 459 }); err != nil { 460 if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { 461 return err 462 } 463 return fmt.Errorf("newIssue: %v", err) 464 } 465 466 pr.Index = issue.Index 467 pr.BaseRepo = repo 468 pr.IssueID = issue.ID 469 if err = db.Insert(ctx, pr); err != nil { 470 return fmt.Errorf("insert pull repo: %v", err) 471 } 472 473 if err = committer.Commit(); err != nil { 474 return fmt.Errorf("Commit: %v", err) 475 } 476 477 return nil 478} 479 480// GetUnmergedPullRequest returns a pull request that is open and has not been merged 481// by given head/base and repo/branch. 482func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) { 483 pr := new(PullRequest) 484 has, err := db.GetEngine(db.DefaultContext). 485 Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?", 486 headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false). 487 Join("INNER", "issue", "issue.id=pull_request.issue_id"). 488 Get(pr) 489 if err != nil { 490 return nil, err 491 } else if !has { 492 return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} 493 } 494 495 return pr, nil 496} 497 498// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status) 499// by given head information (repo and branch). 500func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { 501 pr := new(PullRequest) 502 has, err := db.GetEngine(db.DefaultContext). 503 Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub). 504 OrderBy("id DESC"). 505 Get(pr) 506 if !has { 507 return nil, err 508 } 509 return pr, err 510} 511 512// GetPullRequestByIndex returns a pull request by the given index 513func GetPullRequestByIndex(repoID, index int64) (*PullRequest, error) { 514 if index < 1 { 515 return nil, ErrPullRequestNotExist{} 516 } 517 pr := &PullRequest{ 518 BaseRepoID: repoID, 519 Index: index, 520 } 521 522 has, err := db.GetEngine(db.DefaultContext).Get(pr) 523 if err != nil { 524 return nil, err 525 } else if !has { 526 return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} 527 } 528 529 if err = pr.LoadAttributes(); err != nil { 530 return nil, err 531 } 532 if err = pr.LoadIssue(); err != nil { 533 return nil, err 534 } 535 536 return pr, nil 537} 538 539func getPullRequestByID(e db.Engine, id int64) (*PullRequest, error) { 540 pr := new(PullRequest) 541 has, err := e.ID(id).Get(pr) 542 if err != nil { 543 return nil, err 544 } else if !has { 545 return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""} 546 } 547 return pr, pr.loadAttributes(e) 548} 549 550// GetPullRequestByID returns a pull request by given ID. 551func GetPullRequestByID(id int64) (*PullRequest, error) { 552 return getPullRequestByID(db.GetEngine(db.DefaultContext), id) 553} 554 555// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID. 556func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) { 557 var pr PullRequest 558 has, err := db.GetEngine(db.DefaultContext).Where("issue_id = ?", issueID).Get(&pr) 559 if err != nil { 560 return nil, err 561 } 562 if !has { 563 return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} 564 } 565 return &pr, nil 566} 567 568func getPullRequestByIssueID(e db.Engine, issueID int64) (*PullRequest, error) { 569 pr := &PullRequest{ 570 IssueID: issueID, 571 } 572 has, err := e.Get(pr) 573 if err != nil { 574 return nil, err 575 } else if !has { 576 return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} 577 } 578 return pr, pr.loadAttributes(e) 579} 580 581// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request 582// By poster id. 583func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) { 584 pulls := make([]*PullRequest, 0, 10) 585 586 err := db.GetEngine(db.DefaultContext). 587 Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?", 588 false, PullRequestFlowAGit, false, uid). 589 Join("INNER", "issue", "issue.id=pull_request.issue_id"). 590 Find(&pulls) 591 592 return pulls, err 593} 594 595// GetPullRequestByIssueID returns pull request by given issue ID. 596func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) { 597 return getPullRequestByIssueID(db.GetEngine(db.DefaultContext), issueID) 598} 599 600// Update updates all fields of pull request. 601func (pr *PullRequest) Update() error { 602 _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).AllCols().Update(pr) 603 return err 604} 605 606// UpdateCols updates specific fields of pull request. 607func (pr *PullRequest) UpdateCols(cols ...string) error { 608 _, err := db.GetEngine(db.DefaultContext).ID(pr.ID).Cols(cols...).Update(pr) 609 return err 610} 611 612// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged 613func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error { 614 _, err := db.GetEngine(db.DefaultContext).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) 615 return err 616} 617 618// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title 619func (pr *PullRequest) IsWorkInProgress() bool { 620 if err := pr.LoadIssue(); err != nil { 621 log.Error("LoadIssue: %v", err) 622 return false 623 } 624 return HasWorkInProgressPrefix(pr.Issue.Title) 625} 626 627// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix 628func HasWorkInProgressPrefix(title string) bool { 629 for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { 630 if strings.HasPrefix(strings.ToUpper(title), prefix) { 631 return true 632 } 633 } 634 return false 635} 636 637// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. 638func (pr *PullRequest) IsFilesConflicted() bool { 639 return len(pr.ConflictedFiles) > 0 640} 641 642// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress. 643// It returns an empty string when none were found 644func (pr *PullRequest) GetWorkInProgressPrefix() string { 645 if err := pr.LoadIssue(); err != nil { 646 log.Error("LoadIssue: %v", err) 647 return "" 648 } 649 650 for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { 651 if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) { 652 return pr.Issue.Title[0:len(prefix)] 653 } 654 } 655 return "" 656} 657 658// UpdateCommitDivergence update Divergence of a pull request 659func (pr *PullRequest) UpdateCommitDivergence(ahead, behind int) error { 660 return pr.updateCommitDivergence(db.GetEngine(db.DefaultContext), ahead, behind) 661} 662 663func (pr *PullRequest) updateCommitDivergence(e db.Engine, ahead, behind int) error { 664 if pr.ID == 0 { 665 return fmt.Errorf("pull ID is 0") 666 } 667 pr.CommitsAhead = ahead 668 pr.CommitsBehind = behind 669 _, err := e.ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr) 670 return err 671} 672 673// IsSameRepo returns true if base repo and head repo is the same 674func (pr *PullRequest) IsSameRepo() bool { 675 return pr.BaseRepoID == pr.HeadRepoID 676} 677 678// GetBaseBranchHTMLURL returns the HTML URL of the base branch 679func (pr *PullRequest) GetBaseBranchHTMLURL() string { 680 if err := pr.LoadBaseRepo(); err != nil { 681 log.Error("LoadBaseRepo: %v", err) 682 return "" 683 } 684 if pr.BaseRepo == nil { 685 return "" 686 } 687 return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch) 688} 689 690// GetHeadBranchHTMLURL returns the HTML URL of the head branch 691func (pr *PullRequest) GetHeadBranchHTMLURL() string { 692 if pr.Flow == PullRequestFlowAGit { 693 return "" 694 } 695 696 if err := pr.LoadHeadRepo(); err != nil { 697 log.Error("LoadHeadRepo: %v", err) 698 return "" 699 } 700 if pr.HeadRepo == nil { 701 return "" 702 } 703 return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch) 704} 705