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

[WIP] Add merge/rebase feature #139

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions handler/comment_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
assignReviewerConstant string = "AssignReviewer"
unassignReviewerConstant string = "UnassignReviewer"
messageConstant string = "message"
mergePRConstant string = "Merge"

noDCO string = "no-dco"
labelLimitDefault int = 5
Expand Down Expand Up @@ -120,16 +121,34 @@ func HandleComment(req types.IssueCommentOuter, config config.Config, derekConfi
feedback, err = createMessage(req, command.Type, command.Value, config, derekConfig)
break

case mergePRConstant:
merger := merge{
Config: config,
RepoConfig: derekConfig,
}

feedback, err = merger.Merge(req, command.Type, command.Value)

if len(feedback) > 0 {
log.Printf("Feedback: %s\n", feedback)
}

if err != nil {
log.Println(err)
}

break

default:
feedback = "Unable to work with comment: " + req.Comment.Body
err = nil
break
}

fmt.Print(feedback)
log.Print(feedback)

if err != nil {
fmt.Println(err)
log.Println(err)
}
}

Expand Down Expand Up @@ -434,6 +453,8 @@ func parse(body string, commandTriggers []string) *types.CommentAction {
commandTrigger + "clear reviewer: ": unassignReviewerConstant,
commandTrigger + "message: ": messageConstant,
commandTrigger + "msg: ": messageConstant,
commandTrigger + "merge": mergePRConstant,
commandTrigger + "rebase": mergePRConstant,
}

for trigger, commandType := range commands {
Expand Down
159 changes: 159 additions & 0 deletions handler/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package handler

import (
"context"
"fmt"
"log"

"github.com/alexellis/derek/config"
"github.com/alexellis/derek/types"
"github.com/google/go-github/github"
)

type merge struct {
Config config.Config
RepoConfig *types.DerekRepoConfig
}

func (m *merge) Merge(req types.IssueCommentOuter, cmdType string, cmdValue string) (string, error) {
result := ""

client, ctx := makeClient(req.Installation.ID, m.Config)

if req.Issue.PullRequest == nil {
return "can't merge a non-PR issue", nil
}

if len(m.RepoConfig.Mergers) == 0 {
return "can't merge without at least one merger", nil
}

if mayMerge(req.Comment.User.Login, m.RepoConfig.Mergers) == false {
return fmt.Sprintf("user %s, may not merge", req.Comment.User.Login), nil
}

pr, _, err := client.PullRequests.Get(ctx, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number)
if err != nil {
return "unable to get pull request", err
}

if pr.GetMerged() == false {

if pr.GetMergeable() == true {

if validMergePolicy(req) == false {
sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number,
"I am unable to merge this PR due to merge-policy exception(s)")

return "invalid merge policy", nil
}

mustApprove := []string{}

// An approver, can't approve their own PRs so must come out
// of the list
for _, approver := range m.RepoConfig.MustApprove {
if approver != pr.GetUser().GetLogin() {
mustApprove = append(mustApprove, approver)
}
}

if len(mustApprove) > 0 {

listOpts := &github.ListOptions{}
reviews, _, listReviewsErr := client.PullRequests.ListReviews(ctx, req.Repository.Owner.Login,
req.Repository.Name, req.Issue.Number, listOpts)

if listReviewsErr != nil {
return fmt.Sprintf("unable to list reviews for %d", pr.GetID()), listReviewsErr
}

mustApproveConfirmed := []github.PullRequestReview{}
for _, r := range reviews {
fmt.Printf("Review state: %s, commitID: %s, HEADSHA: %s\n", r.GetState(), r.GetCommitID(), pr.GetHead().GetSHA())
for _, approver := range mustApprove {
if r.GetState() == "APPROVED" &&
r.GetUser().GetLogin() == approver &&
r.GetCommitID() == pr.GetHead().GetSHA() {
mustApproveConfirmed = append(mustApproveConfirmed, *r)
}
}
}

if len(m.RepoConfig.MustApprove) != len(mustApproveConfirmed) {
return fmt.Sprintf("needed %d approvals, but had: %d",
len(m.RepoConfig.MustApprove), len(mustApproveConfirmed)), nil
}
}

pullRequestOptions := github.PullRequestOptions{
MergeMethod: "rebase",
CommitTitle: fmt.Sprintf("Merge PR #%d", req.Issue.Number),
}

mergeRes, _, err := client.PullRequests.Merge(ctx,
req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number,
fmt.Sprintf(`Merging PR #%d by Derek

This is an automated merge by the bot Derek, find more
https://github.com/alexellis/derek/

Signed-off-by: Derek <[email protected]>`, req.Issue.Number), &pullRequestOptions)

if err != nil {

body := fmt.Sprintf(`I have been unable to merge the requested PR: %s`, err.Error())

sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number,
body)

return fmt.Sprintf("Merge issue: %s, %t", mergeRes.GetMessage(), mergeRes.GetMerged()), err
}

sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number,
`I have merged the pull request using the rebase strategy.`)
} else {
sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number,
"This pull request cannot be merged. Rebase your work and try again.")
}
}

return result, err
}

func sendComment(client *github.Client, login string, repo string, issue int, comment string) {

issueComment := &github.IssueComment{
Body: &comment,
}

_, _, err := client.Issues.CreateComment(context.Background(),
login, repo, issue, issueComment)
if err != nil {
log.Printf("Error creating comment %s %s %d\n", login, repo, issue)
}
}

func validMergePolicy(req types.IssueCommentOuter) bool {
validDCO := true
for _, label := range req.Issue.Labels {
if label.Name == "no-dco" {
validDCO = false
break
}
}

return validDCO
}

func mayMerge(user string, list []string) bool {
may := false

for _, item := range list {
if item == user {
may = true
break
}
}
return may
}
1 change: 1 addition & 0 deletions kumbaya
Submodule kumbaya added at a4d0fb
20 changes: 16 additions & 4 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ type Issue struct {
State string `json:"state"`
Milestone Milestone `json:"milestone"`
URL string `json:"url"`

PullRequest *PullRequestIssueLink `json:"pull_request"`
}

type PullRequestIssueLink struct {
URL string `json:"url"`
}

type Milestone struct {
Expand All @@ -79,21 +85,27 @@ type CommentAction struct {
type DerekRepoConfig struct {

// A redirect URL to load the config from another location.
Redirect string
Redirect string `yaml:"redirect"`

// Features can be turned on/off if needed.
Features []string
Features []string `yaml:"features"`

// Users who are enrolled to make use of Derek
Maintainers []string
Maintainers []string `yaml:"maintainers"`

// Curators is an alias for Maintainers and is only used if the Maintainers list is empty.
Curators []string
Curators []string `yaml:"curators"`

//ContributingURL url to contribution guide
ContributingURL string `yaml:"contributing_url"`

Messages []Message `yaml:"custom_messages"`

// Mergers are those who can perform a rebase
Mergers []string `yaml:"mergers"`

// MustApprove are those from whom an approval must be in place for a merge
MustApprove []string `yaml:"must_approve"`
}

type Message struct {
Expand Down