forked from NYANDEV/forgejo
76d6184cd0
Backport #15773 Repositories using external issue tracker tend to use numeric issues in commits. To prevent conflicts during issue reference parsing or inside commit hooks, this change respects these configuration and uses the ! character to refer to pull requests in merge commit messages. For repositories using squash merges, this was already handled. Signed-off-by: JustusBunsi <61625851+justusbunsi@users.noreply.github.com> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Steven <61625851+justusbunsi@users.noreply.github.com>
682 lines
20 KiB
Go
682 lines
20 KiB
Go
// Copyright 2015 The Gogs Authors. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
// PullRequestType defines pull request type
|
|
type PullRequestType int
|
|
|
|
// Enumerate all the pull request types
|
|
const (
|
|
PullRequestGitea PullRequestType = iota
|
|
PullRequestGit
|
|
)
|
|
|
|
// PullRequestStatus defines pull request status
|
|
type PullRequestStatus int
|
|
|
|
// Enumerate all the pull request status
|
|
const (
|
|
PullRequestStatusConflict PullRequestStatus = iota
|
|
PullRequestStatusChecking
|
|
PullRequestStatusMergeable
|
|
PullRequestStatusManuallyMerged
|
|
PullRequestStatusError
|
|
PullRequestStatusEmpty
|
|
)
|
|
|
|
// PullRequest represents relation between pull request and repositories.
|
|
type PullRequest struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
Type PullRequestType
|
|
Status PullRequestStatus
|
|
ConflictedFiles []string `xorm:"TEXT JSON"`
|
|
CommitsAhead int
|
|
CommitsBehind int
|
|
|
|
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
|
|
|
IssueID int64 `xorm:"INDEX"`
|
|
Issue *Issue `xorm:"-"`
|
|
Index int64
|
|
|
|
HeadRepoID int64 `xorm:"INDEX"`
|
|
HeadRepo *Repository `xorm:"-"`
|
|
BaseRepoID int64 `xorm:"INDEX"`
|
|
BaseRepo *Repository `xorm:"-"`
|
|
HeadBranch string
|
|
BaseBranch string
|
|
ProtectedBranch *ProtectedBranch `xorm:"-"`
|
|
MergeBase string `xorm:"VARCHAR(40)"`
|
|
|
|
HasMerged bool `xorm:"INDEX"`
|
|
MergedCommitID string `xorm:"VARCHAR(40)"`
|
|
MergerID int64 `xorm:"INDEX"`
|
|
Merger *User `xorm:"-"`
|
|
MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"`
|
|
|
|
isHeadRepoLoaded bool `xorm:"-"`
|
|
}
|
|
|
|
// MustHeadUserName returns the HeadRepo's username if failed return blank
|
|
func (pr *PullRequest) MustHeadUserName() string {
|
|
if err := pr.LoadHeadRepo(); err != nil {
|
|
if !IsErrRepoNotExist(err) {
|
|
log.Error("LoadHeadRepo: %v", err)
|
|
} else {
|
|
log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err)
|
|
}
|
|
return ""
|
|
}
|
|
if pr.HeadRepo == nil {
|
|
return ""
|
|
}
|
|
return pr.HeadRepo.OwnerName
|
|
}
|
|
|
|
// Note: don't try to get Issue because will end up recursive querying.
|
|
func (pr *PullRequest) loadAttributes(e Engine) (err error) {
|
|
if pr.HasMerged && pr.Merger == nil {
|
|
pr.Merger, err = getUserByID(e, pr.MergerID)
|
|
if IsErrUserNotExist(err) {
|
|
pr.MergerID = -1
|
|
pr.Merger = NewGhostUser()
|
|
} else if err != nil {
|
|
return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadAttributes loads pull request attributes from database
|
|
func (pr *PullRequest) LoadAttributes() error {
|
|
return pr.loadAttributes(x)
|
|
}
|
|
|
|
func (pr *PullRequest) loadHeadRepo(e Engine) (err error) {
|
|
if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
|
if pr.HeadRepoID == pr.BaseRepoID {
|
|
if pr.BaseRepo != nil {
|
|
pr.HeadRepo = pr.BaseRepo
|
|
return nil
|
|
} else if pr.Issue != nil && pr.Issue.Repo != nil {
|
|
pr.HeadRepo = pr.Issue.Repo
|
|
return nil
|
|
}
|
|
}
|
|
|
|
pr.HeadRepo, err = getRepositoryByID(e, pr.HeadRepoID)
|
|
if err != nil && !IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work
|
|
return fmt.Errorf("getRepositoryByID(head): %v", err)
|
|
}
|
|
pr.isHeadRepoLoaded = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadHeadRepo loads the head repository
|
|
func (pr *PullRequest) LoadHeadRepo() error {
|
|
return pr.loadHeadRepo(x)
|
|
}
|
|
|
|
// LoadBaseRepo loads the target repository
|
|
func (pr *PullRequest) LoadBaseRepo() error {
|
|
return pr.loadBaseRepo(x)
|
|
}
|
|
|
|
func (pr *PullRequest) loadBaseRepo(e Engine) (err error) {
|
|
if pr.BaseRepo != nil {
|
|
return nil
|
|
}
|
|
|
|
if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil {
|
|
pr.BaseRepo = pr.HeadRepo
|
|
return nil
|
|
}
|
|
|
|
if pr.Issue != nil && pr.Issue.Repo != nil {
|
|
pr.BaseRepo = pr.Issue.Repo
|
|
return nil
|
|
}
|
|
|
|
pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID)
|
|
if err != nil {
|
|
return fmt.Errorf("GetRepositoryByID(base): %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadIssue loads issue information from database
|
|
func (pr *PullRequest) LoadIssue() (err error) {
|
|
return pr.loadIssue(x)
|
|
}
|
|
|
|
func (pr *PullRequest) loadIssue(e Engine) (err error) {
|
|
if pr.Issue != nil {
|
|
return nil
|
|
}
|
|
|
|
pr.Issue, err = getIssueByID(e, pr.IssueID)
|
|
if err == nil {
|
|
pr.Issue.PullRequest = pr
|
|
}
|
|
return err
|
|
}
|
|
|
|
// LoadProtectedBranch loads the protected branch of the base branch
|
|
func (pr *PullRequest) LoadProtectedBranch() (err error) {
|
|
return pr.loadProtectedBranch(x)
|
|
}
|
|
|
|
func (pr *PullRequest) loadProtectedBranch(e Engine) (err error) {
|
|
if pr.ProtectedBranch == nil {
|
|
if pr.BaseRepo == nil {
|
|
if pr.BaseRepoID == 0 {
|
|
return nil
|
|
}
|
|
pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
pr.ProtectedBranch, err = getProtectedBranchBy(e, pr.BaseRepo.ID, pr.BaseBranch)
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetDefaultMergeMessage returns default message used when merging pull request
|
|
func (pr *PullRequest) GetDefaultMergeMessage() string {
|
|
if pr.HeadRepo == nil {
|
|
var err error
|
|
pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID)
|
|
if err != nil {
|
|
log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
|
|
return ""
|
|
}
|
|
}
|
|
if err := pr.LoadIssue(); err != nil {
|
|
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
|
|
return ""
|
|
}
|
|
if err := pr.LoadBaseRepo(); err != nil {
|
|
log.Error("LoadBaseRepo: %v", err)
|
|
return ""
|
|
}
|
|
|
|
issueReference := "#"
|
|
if pr.BaseRepo.UnitEnabled(UnitTypeExternalTracker) {
|
|
issueReference = "!"
|
|
}
|
|
|
|
if pr.BaseRepoID == pr.HeadRepoID {
|
|
return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// ReviewCount represents a count of Reviews
|
|
type ReviewCount struct {
|
|
IssueID int64
|
|
Type ReviewType
|
|
Count int64
|
|
}
|
|
|
|
// GetApprovalCounts returns the approval counts by type
|
|
// FIXME: Only returns official counts due to double counting of non-official counts
|
|
func (pr *PullRequest) GetApprovalCounts() ([]*ReviewCount, error) {
|
|
return pr.getApprovalCounts(x)
|
|
}
|
|
|
|
func (pr *PullRequest) getApprovalCounts(e Engine) ([]*ReviewCount, error) {
|
|
rCounts := make([]*ReviewCount, 0, 6)
|
|
sess := e.Where("issue_id = ?", pr.IssueID)
|
|
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)
|
|
}
|
|
|
|
// GetApprovers returns the approvers of the pull request
|
|
func (pr *PullRequest) GetApprovers() string {
|
|
stringBuilder := strings.Builder{}
|
|
if err := pr.getReviewedByLines(&stringBuilder); err != nil {
|
|
log.Error("Unable to getReviewedByLines: Error: %v", err)
|
|
return ""
|
|
}
|
|
|
|
return stringBuilder.String()
|
|
}
|
|
|
|
func (pr *PullRequest) getReviewedByLines(writer io.Writer) error {
|
|
maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers
|
|
|
|
if maxReviewers == 0 {
|
|
return nil
|
|
}
|
|
|
|
sess := x.NewSession()
|
|
defer sess.Close()
|
|
if err := sess.Begin(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Note: This doesn't page as we only expect a very limited number of reviews
|
|
reviews, err := findReviews(sess, FindReviewOptions{
|
|
Type: ReviewTypeApprove,
|
|
IssueID: pr.IssueID,
|
|
OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly,
|
|
})
|
|
if err != nil {
|
|
log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err)
|
|
return err
|
|
}
|
|
|
|
reviewersWritten := 0
|
|
|
|
for _, review := range reviews {
|
|
if maxReviewers > 0 && reviewersWritten > maxReviewers {
|
|
break
|
|
}
|
|
|
|
if err := review.loadReviewer(sess); err != nil && !IsErrUserNotExist(err) {
|
|
log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err)
|
|
return err
|
|
} else if review.Reviewer == nil {
|
|
continue
|
|
}
|
|
if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil {
|
|
return err
|
|
}
|
|
if _, err := writer.Write([]byte{'\n'}); err != nil {
|
|
return err
|
|
}
|
|
reviewersWritten++
|
|
}
|
|
return sess.Commit()
|
|
}
|
|
|
|
// GetDefaultSquashMessage returns default message used when squash and merging pull request
|
|
func (pr *PullRequest) GetDefaultSquashMessage() string {
|
|
if err := pr.LoadIssue(); err != nil {
|
|
log.Error("LoadIssue: %v", err)
|
|
return ""
|
|
}
|
|
if err := pr.LoadBaseRepo(); err != nil {
|
|
log.Error("LoadBaseRepo: %v", err)
|
|
return ""
|
|
}
|
|
if pr.BaseRepo.UnitEnabled(UnitTypeExternalTracker) {
|
|
return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index)
|
|
}
|
|
return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index)
|
|
}
|
|
|
|
// GetGitRefName returns git ref for hidden pull request branch
|
|
func (pr *PullRequest) GetGitRefName() string {
|
|
return fmt.Sprintf("refs/pull/%d/head", pr.Index)
|
|
}
|
|
|
|
// IsChecking returns true if this pull request is still checking conflict.
|
|
func (pr *PullRequest) IsChecking() bool {
|
|
return pr.Status == PullRequestStatusChecking
|
|
}
|
|
|
|
// CanAutoMerge returns true if this pull request can be merged automatically.
|
|
func (pr *PullRequest) CanAutoMerge() bool {
|
|
return pr.Status == PullRequestStatusMergeable
|
|
}
|
|
|
|
// IsEmpty returns true if this pull request is empty.
|
|
func (pr *PullRequest) IsEmpty() bool {
|
|
return pr.Status == PullRequestStatusEmpty
|
|
}
|
|
|
|
// MergeStyle represents the approach to merge commits into base branch.
|
|
type MergeStyle string
|
|
|
|
const (
|
|
// MergeStyleMerge create merge commit
|
|
MergeStyleMerge MergeStyle = "merge"
|
|
// MergeStyleRebase rebase before merging
|
|
MergeStyleRebase MergeStyle = "rebase"
|
|
// MergeStyleRebaseMerge rebase before merging with merge commit (--no-ff)
|
|
MergeStyleRebaseMerge MergeStyle = "rebase-merge"
|
|
// MergeStyleSquash squash commits into single commit before merging
|
|
MergeStyleSquash MergeStyle = "squash"
|
|
// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
|
|
MergeStyleManuallyMerged MergeStyle = "manually-merged"
|
|
)
|
|
|
|
// SetMerged sets a pull request to merged and closes the corresponding issue
|
|
func (pr *PullRequest) SetMerged() (bool, error) {
|
|
if pr.HasMerged {
|
|
return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index)
|
|
}
|
|
if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil {
|
|
return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
|
|
}
|
|
|
|
pr.HasMerged = true
|
|
|
|
sess := x.NewSession()
|
|
defer sess.Close()
|
|
if err := sess.Begin(); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
pr.Issue = nil
|
|
if err := pr.loadIssue(sess); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if tmpPr, err := getPullRequestByID(sess, pr.ID); err != nil {
|
|
return false, err
|
|
} else if tmpPr.HasMerged {
|
|
if pr.Issue.IsClosed {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID)
|
|
} else if pr.Issue.IsClosed {
|
|
return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index)
|
|
}
|
|
|
|
if err := pr.Issue.loadRepo(sess); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := pr.Issue.Repo.getOwner(sess); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if _, err := pr.Issue.changeStatus(sess, pr.Merger, true, true); err != nil {
|
|
return false, fmt.Errorf("Issue.changeStatus: %v", err)
|
|
}
|
|
|
|
// 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.
|
|
if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix").Update(pr); err != nil {
|
|
return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err)
|
|
}
|
|
|
|
if err := sess.Commit(); err != nil {
|
|
return false, fmt.Errorf("Commit: %v", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// NewPullRequest creates new pull request with labels for repository.
|
|
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
|
|
// Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887
|
|
i := 0
|
|
for {
|
|
if err = newPullRequestAttempt(repo, pull, labelIDs, uuids, pr); err == nil {
|
|
return nil
|
|
}
|
|
if !IsErrNewIssueInsert(err) {
|
|
return err
|
|
}
|
|
if i++; i == issueMaxDupIndexAttempts {
|
|
break
|
|
}
|
|
log.Error("NewPullRequest: error attempting to insert the new issue; will retry. Original error: %v", err)
|
|
}
|
|
return fmt.Errorf("NewPullRequest: too many errors attempting to insert the new issue. Last error was: %v", err)
|
|
}
|
|
|
|
func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
|
|
sess := x.NewSession()
|
|
defer sess.Close()
|
|
if err = sess.Begin(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = newIssue(sess, pull.Poster, NewIssueOptions{
|
|
Repo: repo,
|
|
Issue: pull,
|
|
LabelIDs: labelIDs,
|
|
Attachments: uuids,
|
|
IsPull: true,
|
|
}); err != nil {
|
|
if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("newIssue: %v", err)
|
|
}
|
|
|
|
pr.Index = pull.Index
|
|
pr.BaseRepo = repo
|
|
|
|
pr.IssueID = pull.ID
|
|
if _, err = sess.Insert(pr); err != nil {
|
|
return fmt.Errorf("insert pull repo: %v", err)
|
|
}
|
|
|
|
if err = sess.Commit(); err != nil {
|
|
return fmt.Errorf("Commit: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUnmergedPullRequest returns a pull request that is open and has not been merged
|
|
// by given head/base and repo/branch.
|
|
func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) {
|
|
pr := new(PullRequest)
|
|
has, err := x.
|
|
Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
|
|
headRepoID, headBranch, baseRepoID, baseBranch, false, false).
|
|
Join("INNER", "issue", "issue.id=pull_request.issue_id").
|
|
Get(pr)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch}
|
|
}
|
|
|
|
return pr, nil
|
|
}
|
|
|
|
// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status)
|
|
// by given head information (repo and branch).
|
|
func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) {
|
|
pr := new(PullRequest)
|
|
has, err := x.
|
|
Where("head_repo_id = ? AND head_branch = ?", repoID, branch).
|
|
OrderBy("id DESC").
|
|
Get(pr)
|
|
if !has {
|
|
return nil, err
|
|
}
|
|
return pr, err
|
|
}
|
|
|
|
// GetPullRequestByIndex returns a pull request by the given index
|
|
func GetPullRequestByIndex(repoID, index int64) (*PullRequest, error) {
|
|
pr := &PullRequest{
|
|
BaseRepoID: repoID,
|
|
Index: index,
|
|
}
|
|
|
|
has, err := x.Get(pr)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
|
|
}
|
|
|
|
if err = pr.LoadAttributes(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = pr.LoadIssue(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pr, nil
|
|
}
|
|
|
|
func getPullRequestByID(e Engine, id int64) (*PullRequest, error) {
|
|
pr := new(PullRequest)
|
|
has, err := e.ID(id).Get(pr)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""}
|
|
}
|
|
return pr, pr.loadAttributes(e)
|
|
}
|
|
|
|
// GetPullRequestByID returns a pull request by given ID.
|
|
func GetPullRequestByID(id int64) (*PullRequest, error) {
|
|
return getPullRequestByID(x, id)
|
|
}
|
|
|
|
// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
|
|
func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) {
|
|
var pr PullRequest
|
|
has, err := x.Where("issue_id = ?", issueID).Get(&pr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
|
}
|
|
return &pr, nil
|
|
}
|
|
|
|
func getPullRequestByIssueID(e Engine, issueID int64) (*PullRequest, error) {
|
|
pr := &PullRequest{
|
|
IssueID: issueID,
|
|
}
|
|
has, err := e.Get(pr)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
|
}
|
|
return pr, pr.loadAttributes(e)
|
|
}
|
|
|
|
// GetPullRequestByIssueID returns pull request by given issue ID.
|
|
func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) {
|
|
return getPullRequestByIssueID(x, issueID)
|
|
}
|
|
|
|
// Update updates all fields of pull request.
|
|
func (pr *PullRequest) Update() error {
|
|
_, err := x.ID(pr.ID).AllCols().Update(pr)
|
|
return err
|
|
}
|
|
|
|
// UpdateCols updates specific fields of pull request.
|
|
func (pr *PullRequest) UpdateCols(cols ...string) error {
|
|
_, err := x.ID(pr.ID).Cols(cols...).Update(pr)
|
|
return err
|
|
}
|
|
|
|
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
|
|
func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error {
|
|
_, err := x.Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
|
|
return err
|
|
}
|
|
|
|
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
|
|
func (pr *PullRequest) IsWorkInProgress() bool {
|
|
if err := pr.LoadIssue(); err != nil {
|
|
log.Error("LoadIssue: %v", err)
|
|
return false
|
|
}
|
|
|
|
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
|
if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
|
func (pr *PullRequest) IsFilesConflicted() bool {
|
|
return len(pr.ConflictedFiles) > 0
|
|
}
|
|
|
|
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
|
|
// It returns an empty string when none were found
|
|
func (pr *PullRequest) GetWorkInProgressPrefix() string {
|
|
if err := pr.LoadIssue(); err != nil {
|
|
log.Error("LoadIssue: %v", err)
|
|
return ""
|
|
}
|
|
|
|
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
|
if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
|
|
return pr.Issue.Title[0:len(prefix)]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UpdateCommitDivergence update Divergence of a pull request
|
|
func (pr *PullRequest) UpdateCommitDivergence(ahead, behind int) error {
|
|
return pr.updateCommitDivergence(x, ahead, behind)
|
|
}
|
|
|
|
func (pr *PullRequest) updateCommitDivergence(e Engine, ahead, behind int) error {
|
|
if pr.ID == 0 {
|
|
return fmt.Errorf("pull ID is 0")
|
|
}
|
|
pr.CommitsAhead = ahead
|
|
pr.CommitsBehind = behind
|
|
_, err := e.ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr)
|
|
return err
|
|
}
|
|
|
|
// IsSameRepo returns true if base repo and head repo is the same
|
|
func (pr *PullRequest) IsSameRepo() bool {
|
|
return pr.BaseRepoID == pr.HeadRepoID
|
|
}
|
|
|
|
// GetBaseBranchHTMLURL returns the HTML URL of the base branch
|
|
func (pr *PullRequest) GetBaseBranchHTMLURL() string {
|
|
if err := pr.LoadBaseRepo(); err != nil {
|
|
log.Error("LoadBaseRepo: %v", err)
|
|
return ""
|
|
}
|
|
if pr.BaseRepo == nil {
|
|
return ""
|
|
}
|
|
return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch)
|
|
}
|
|
|
|
// GetHeadBranchHTMLURL returns the HTML URL of the head branch
|
|
func (pr *PullRequest) GetHeadBranchHTMLURL() string {
|
|
if err := pr.LoadHeadRepo(); err != nil {
|
|
log.Error("LoadHeadRepo: %v", err)
|
|
return ""
|
|
}
|
|
if pr.HeadRepo == nil {
|
|
return ""
|
|
}
|
|
return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch)
|
|
}
|