Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move checks for pulls before merge into own function #19271

Merged
12 changes: 6 additions & 6 deletions models/error.go
Expand Up @@ -586,18 +586,18 @@ func (err ErrBranchesEqual) Error() string {
return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName)
}

// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
type ErrNotAllowedToMerge struct {
// ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
type ErrDisallowedToMerge struct {
Reason string
}

// IsErrNotAllowedToMerge checks if an error is an ErrNotAllowedToMerge.
func IsErrNotAllowedToMerge(err error) bool {
_, ok := err.(ErrNotAllowedToMerge)
// IsErrDisallowedToMerge checks if an error is an ErrDisallowedToMerge.
func IsErrDisallowedToMerge(err error) bool {
_, ok := err.(ErrDisallowedToMerge)
return ok
}

func (err ErrNotAllowedToMerge) Error() string {
func (err ErrDisallowedToMerge) Error() string {
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
}

Expand Down
27 changes: 11 additions & 16 deletions models/pull.go
Expand Up @@ -222,22 +222,19 @@ func (pr *PullRequest) loadProtectedBranch(ctx context.Context) (err error) {
}

// GetDefaultMergeMessage returns default message used when merging pull request
func (pr *PullRequest) GetDefaultMergeMessage() string {
func (pr *PullRequest) GetDefaultMergeMessage() (string, error) {
if pr.HeadRepo == nil {
var err error
pr.HeadRepo, err = repo_model.GetRepositoryByID(pr.HeadRepoID)
if err != nil {
log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
return ""
return "", fmt.Errorf("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
}
}
if err := pr.LoadIssue(); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
return "", fmt.Errorf("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
}
if err := pr.LoadBaseRepo(); err != nil {
log.Error("LoadBaseRepo: %v", err)
return ""
return "", fmt.Errorf("LoadBaseRepo: %v", err)
}

issueReference := "#"
Expand All @@ -246,10 +243,10 @@ func (pr *PullRequest) GetDefaultMergeMessage() string {
}

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 into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil
}

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)
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), nil
}

// ReviewCount represents a count of Reviews
Expand Down Expand Up @@ -335,19 +332,17 @@ func (pr *PullRequest) getReviewedByLines(writer io.Writer) error {
}

// GetDefaultSquashMessage returns default message used when squash and merging pull request
func (pr *PullRequest) GetDefaultSquashMessage() string {
func (pr *PullRequest) GetDefaultSquashMessage() (string, error) {
if err := pr.LoadIssue(); err != nil {
log.Error("LoadIssue: %v", err)
return ""
return "", fmt.Errorf("LoadIssue: %v", err)
}
if err := pr.LoadBaseRepo(); err != nil {
log.Error("LoadBaseRepo: %v", err)
return ""
return "", fmt.Errorf("LoadBaseRepo: %v", err)
}
if pr.BaseRepo.UnitEnabled(unit.TypeExternalTracker) {
return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index)
return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index), nil
}
return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index)
return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index), nil
}

// GetGitRefName returns git ref for hidden pull request branch
Expand Down
16 changes: 12 additions & 4 deletions models/pull_test.go
Expand Up @@ -261,11 +261,15 @@ func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)

assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", pr.GetDefaultMergeMessage())
msg, err := pr.GetDefaultMergeMessage()
assert.NoError(t, err)
assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", msg)

pr.BaseRepoID = 1
pr.HeadRepoID = 2
assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", pr.GetDefaultMergeMessage())
msg, err = pr.GetDefaultMergeMessage()
assert.NoError(t, err)
assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", msg)
}

func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
Expand All @@ -283,9 +287,13 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {

pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2, BaseRepo: baseRepo}).(*PullRequest)

assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", pr.GetDefaultMergeMessage())
msg, err := pr.GetDefaultMergeMessage()
assert.NoError(t, err)
assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", msg)

pr.BaseRepoID = 1
pr.HeadRepoID = 2
assert.Equal(t, "Merge pull request 'issue3' (!3) from user2/repo1:branch2 into master", pr.GetDefaultMergeMessage())
msg, err = pr.GetDefaultMergeMessage()
assert.NoError(t, err)
assert.Equal(t, "Merge pull request 'issue3' (!3) from user2/repo1:branch2 into master", msg)
}
103 changes: 28 additions & 75 deletions routers/api/v1/repo/pull.go
Expand Up @@ -723,13 +723,12 @@ func MergePullRequest(ctx *context.APIContext) {
return
}

if err = pr.LoadHeadRepo(); err != nil {
if err := pr.LoadHeadRepo(); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}

err = pr.LoadIssue()
if err != nil {
if err := pr.LoadIssue(); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
Expand All @@ -743,29 +742,33 @@ func MergePullRequest(ctx *context.APIContext) {
}
}

if pr.Issue.IsClosed {
ctx.NotFound()
return
}
manuallMerge := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged
force := form.ForceMerge != nil && *form.ForceMerge

allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsUSerAllowedToMerge", err)
return
}
if !allowedMerge {
ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
return
}

if pr.HasMerged {
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
if err := pull_service.CheckPullProtection(ctx, ctx.Doer, &ctx.Repo.Permission, pr, manuallMerge, force); err != nil {
if pull_service.IsErrIsClosed(err) {
ctx.NotFound()
} else if pull_service.IsErrUserNotAllowedToMerge(err) {
ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
} else if pull_service.IsErrHasMerged(err) {
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
} else if pull_service.IsErrIsWorkInProgress(err) {
ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
} else if pull_service.IsErrNotMergableState(err) {
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
} else if models.IsErrDisallowedToMerge(err) {
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
} else if asymkey_service.IsErrWontSign(err) {
ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err)
} else {
ctx.InternalServerError(err)
}
return
}

// handle manually-merged mark
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged {
if err = pull_service.MergedManually(pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
if manuallMerge {
if err := pull_service.MergedManually(pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
if models.IsErrInvalidMergeStyle(err) {
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
return
Expand All @@ -781,63 +784,13 @@ func MergePullRequest(ctx *context.APIContext) {
return
}

if !pr.CanAutoMerge() {
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
// set defaults to propagate needed fields
if err := form.SetDefaults(pr); err != nil {
ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err))
return
}

if pr.IsWorkInProgress() {
ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
return
}

if err := pull_service.CheckPRReadyToMerge(ctx, pr, false); err != nil {
if !models.IsErrNotAllowedToMerge(err) {
ctx.Error(http.StatusInternalServerError, "CheckPRReadyToMerge", err)
return
}
if form.ForceMerge != nil && *form.ForceMerge {
if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "IsUserRepoAdmin", err)
return
} else if !isRepoAdmin {
ctx.Error(http.StatusMethodNotAllowed, "Merge", "Only repository admin can merge if not all checks are ok (force merge)")
}
} else {
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
return
}
}

if _, err := pull_service.IsSignedIfRequired(ctx, pr, ctx.Doer); err != nil {
if !asymkey_service.IsErrWontSign(err) {
ctx.Error(http.StatusInternalServerError, "IsSignedIfRequired", err)
return
}
ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err)
return
}

if len(form.Do) == 0 {
form.Do = string(repo_model.MergeStyleMerge)
}

message := strings.TrimSpace(form.MergeTitleField)
if len(message) == 0 {
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleMerge {
message = pr.GetDefaultMergeMessage()
}
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleSquash {
message = pr.GetDefaultSquashMessage()
}
}

form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
if len(form.MergeMessageField) > 0 {
message += "\n\n" + form.MergeMessageField
}

if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil {
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil {
if models.IsErrInvalidMergeStyle(err) {
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
return
Expand Down
2 changes: 1 addition & 1 deletion routers/private/hook_pre_receive.go
Expand Up @@ -340,7 +340,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN

// Check all status checks and reviews are ok
if err := pull_service.CheckPRReadyToMerge(ctx, pr, true); err != nil {
if models.IsErrNotAllowedToMerge(err) {
if models.IsErrDisallowedToMerge(err) {
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
ctx.JSON(http.StatusForbidden, private.Response{
Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
Expand Down