Skip to content

Commit

Permalink
Auto merge pull requests when all checks succeeded
Browse files Browse the repository at this point in the history
Signed-off-by: kolaente <k@knt.li>
  • Loading branch information
kolaente committed May 4, 2020
1 parent 99082ee commit a61f329
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 96 deletions.
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ var migrations = []Migration{
NewMigration("Add Branch Protection Block Outdated Branch", addBlockOnOutdatedBranch),
// v138 -> v139
NewMigration("Add ResolveDoerID to Comment table", addResolveDoerIDCommentColumn),
// v139 -> v140
NewMigration("Add auto merge table", addAutoMergeTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
20 changes: 20 additions & 0 deletions models/migrations/v139.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2020 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 migrations

import "xorm.io/xorm"

func addAutoMergeTable(x *xorm.Engine) error {
type MergeStyle string
type ScheduledPullRequestMerge struct {
ID int64 `xorm:"pk autoincr"`
PullID int64 `xorm:"BIGINT"`
UserID int64 `xorm:"BIGINT"`
MergeStyle MergeStyle `xorm:"varchar(50)"`
Message string `xorm:"TEXT"`
}

return x.Sync2(&ScheduledPullRequestMerge{})
}
13 changes: 13 additions & 0 deletions models/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,16 @@ func (pr *PullRequest) updateCommitDivergence(e Engine, ahead, behind int) error
func (pr *PullRequest) IsSameRepo() bool {
return pr.BaseRepoID == pr.HeadRepoID
}

// GetPullRequestByHeadBranch returns a pr by head branch
func GetPullRequestByHeadBranch(headBranch string, repo *Repository) (pr *PullRequest, err error) {
pr = &PullRequest{}
exists, err := x.Where("head_branch = ? AND head_repo_id = ?", headBranch, repo.ID).Get(pr)
if !exists {
return nil, ErrPullRequestNotExist{
HeadBranch: headBranch,
HeadRepoID: repo.ID,
}
}
return
}
55 changes: 55 additions & 0 deletions models/scheduled_pull_request_merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2019 Gitea. 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 "code.gitea.io/gitea/modules/timeutil"

// ScheduledPullRequestMerge represents a pull request scheduled for merging when checks succeed
type ScheduledPullRequestMerge struct {
ID int64 `xorm:"pk autoincr"`
PullID int64 `xorm:"BIGINT"`
UserID int64 `xorm:"BIGINT"`
User *User `xorm:"-"`
MergeStyle MergeStyle `xorm:"varchar(50)"`
Message string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}

// ScheduleAutoMerge schedules a pull request to be merged when all checks succeed
func ScheduleAutoMerge(opts *ScheduledPullRequestMerge) (err error) {
// Check if we already have a merge scheduled for that pull request
exists, err := x.Exist(&ScheduledPullRequestMerge{PullID: opts.PullID})
if err != nil {
return
}
if exists {
// Maybe FIXME: Should we return a custom error here?
return nil
}

_, err = x.Insert(opts)
return err
}

// GetScheduledMergeRequestByPullID gets a scheduled pull request merge by pull request id
func GetScheduledMergeRequestByPullID(pullID int64) (exists bool, scheduledPRM *ScheduledPullRequestMerge, err error) {
scheduledPRM = &ScheduledPullRequestMerge{}
exists, err = x.Where("pull_id = ?", pullID).Get(scheduledPRM)
if err != nil || !exists {
return
}
scheduledPRM.User, err = getUserByID(x, scheduledPRM.UserID)
return
}

// RemoveScheduledMergeRequest cancels a previously scheduled pull request
func RemoveScheduledMergeRequest(scheduledPR *ScheduledPullRequestMerge) (err error) {
if scheduledPR.ID == 0 && scheduledPR.PullID != 0 {
_, err = x.Where("pull_id = ?", scheduledPR.PullID).Delete(&ScheduledPullRequestMerge{})
return
}
_, err = x.Where("id = ? AND pull_id = ?", scheduledPR.ID, scheduledPR.PullID).Delete(&ScheduledPullRequestMerge{})
return
}
9 changes: 5 additions & 4 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,11 @@ func (f *InitializeLabelsForm) Validate(ctx *macaron.Context, errs binding.Error
type MergePullRequestForm struct {
// required: true
// enum: merge,rebase,rebase-merge,squash
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
MergeTitleField string
MergeMessageField string
ForceMerge *bool `json:"force_merge,omitempty"`
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
MergeTitleField string
MergeMessageField string
ForceMerge *bool `json:"force_merge,omitempty"`
MergeWhenChecksSucceed bool
}

// Validate validates the fields
Expand Down
20 changes: 20 additions & 0 deletions modules/git/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
}

// GetBranchName gets the closes branch name (as returned by 'git name-rev')
// FIXME: This get only one branch, but one commit can be part of multiple branches
func (c *Commit) GetBranchName() (string, error) {
data, err := NewCommand("name-rev", c.ID.String()).RunInDirBytes(c.repo.Path)
if err != nil {
Expand All @@ -477,6 +478,25 @@ func (c *Commit) GetBranchName() (string, error) {
return strings.Split(strings.Split(string(data), " ")[1], "~")[0], nil
}

// GetBranchNames returns all branches a commit is part of
func (c *Commit) GetBranchNames() (branchNames []string, err error) {
data, err := NewCommand("name-rev", c.ID.String()).RunInDirBytes(c.repo.Path)
if err != nil {
return
}

namesRaw := strings.Split(string(data), "\n")
for _, s := range namesRaw {
s = strings.TrimSpace(s)
if s == "" {
continue
}
branchNames = append(branchNames, strings.Split(strings.Split(s, " ")[1], "~")[0])
}

return
}

// CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct {
Added []string
Expand Down
8 changes: 8 additions & 0 deletions modules/repofiles/commit_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/automerge"
)

// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
Expand Down Expand Up @@ -37,5 +38,12 @@ func CreateCommitStatus(repo *models.Repository, creator *models.User, sha strin
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
}

if status.State.IsSuccess() {
err = automerge.MergeScheduledPullRequest(sha, repo)
if err != nil {
return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
}
}

return nil
}
13 changes: 13 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,14 @@ pulls.rebase_merge_pull_request = Rebase and Merge
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
pulls.squash_merge_pull_request = Squash and Merge
pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
pulls.merge_pull_request_now = Merge Pull Request Now
pulls.rebase_merge_pull_request_now = Rebase and Merge Now
pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff)
pulls.squash_merge_pull_request_now = Squash and Merge Now
pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed
pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed
pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed
pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed
pulls.invalid_merge_option = You cannot use this merge option for this pull request.
pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<br>%[2]s<br>Hint: Try a different strategy
pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s<br>%[2]s<br>%[3]s<br>Hint:Try a different strategy
Expand All @@ -1154,6 +1162,11 @@ pulls.update_not_allowed = You are not allowed to update branch
pulls.outdated_with_base_branch = This branch is out-of-date with the base branch
pulls.closed_at = `closed this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at = `reopened this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.merge_on_status_success_success = The pull request was successfully scheduled to merge when all checks succeed.
pulls.pr_has_pending_merge_on_success = This pull request has been set to auto merge when all checks succeed by %[1]s %[2]s.
pulls.merge_pull_on_success_cancel = Cancel automatic merge
pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge.
pulls.pull_request_schedule_canceled = This pull request was successfully canceled for auto merge.
milestones.new = New Milestone
milestones.open_tab = %d Open
Expand Down
7 changes: 7 additions & 0 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,13 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("GetReviewersByIssueID", err)
return
}

// Check if there is a pending pr merge
ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = models.GetScheduledMergeRequestByPullID(pull.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
}
}

// Get Dependencies
Expand Down
55 changes: 55 additions & 0 deletions routers/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,34 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
return
}

lastCommitStatus, err := pr.GetLastCommitStatus()
if err != nil {
return
}
if form.MergeWhenChecksSucceed && !lastCommitStatus.State.IsSuccess() {
err = models.ScheduleAutoMerge(&models.ScheduledPullRequestMerge{
PullID: pr.ID,
UserID: ctx.User.ID,
MergeStyle: models.MergeStyle(form.Do),
Message: message,
})
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
}
ctx.Flash.Success(ctx.Tr("repo.pulls.merge_on_status_success_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
}
// Removing an auto merge pull request is something we can execute whether or not a pull request auto merge was
// scheduled before, hece we can remove it without checking for its existence.
err = models.RemoveScheduledMergeRequest(&models.ScheduledPullRequestMerge{PullID: pr.ID})
if err != nil {
ctx.ServerError("RemoveScheduledMergeRequest", err)
return
}

if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
Expand Down Expand Up @@ -860,6 +888,33 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
}

// CancelAutoMergePullRequest cancels a scheduled pr
func CancelAutoMergePullRequest(ctx *context.Context) {
issue := checkPullInfo(ctx)
if ctx.Written() {
return
}
pr := issue.PullRequest
exists, scheduledInfo, err := models.GetScheduledMergeRequestByPullID(pr.ID)
if err != nil {
ctx.ServerError("GetScheduledMergeRequestByPullID", err)
return
}
if !exists {
ctx.Flash.Error(ctx.Tr("repo.pulls.pull_request_not_scheduled"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
}
err = models.RemoveScheduledMergeRequest(scheduledInfo)
if err != nil {
ctx.ServerError("RemoveScheduledMergeRequest", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.pulls.pull_request_schedule_canceled"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
}

func stopTimerIfAvailable(user *models.User, issue *models.Issue) error {

if models.StopwatchExists(user.ID, issue.ID) {
Expand Down
1 change: 1 addition & 0 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get(".patch", repo.DownloadPullPatch)
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest)
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
m.Group("/files", func() {
Expand Down
Loading

0 comments on commit a61f329

Please sign in to comment.