From 945804f800ac4e81c46bde848616e9d9392a9571 Mon Sep 17 00:00:00 2001 From: Lanre Adelowo Date: Thu, 27 Dec 2018 19:04:30 +0100 Subject: [PATCH] Webhook for Pull Request approval/rejection (#5027) --- models/review.go | 50 +++++++++++++++++++++++++++---- models/webhook.go | 22 +++++++------- models/webhook_dingtalk.go | 29 +++++++++++++++++- models/webhook_discord.go | 52 +++++++++++++++++++++++++++++++++ models/webhook_slack.go | 33 +++++++++++++++++++-- options/locale/locale_en-US.ini | 2 +- 6 files changed, 168 insertions(+), 20 deletions(-) diff --git a/models/review.go b/models/review.go index 91b6d6dbb20e..ea2ccebed5d5 100644 --- a/models/review.go +++ b/models/review.go @@ -9,10 +9,11 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - "github.com/go-xorm/core" - "github.com/go-xorm/xorm" + api "code.gitea.io/sdk/gitea" "github.com/go-xorm/builder" + "github.com/go-xorm/core" + "github.com/go-xorm/xorm" ) // ReviewType defines the sort of feedback a review gives @@ -233,6 +234,43 @@ func createReview(e Engine, opts CreateReviewOptions) (*Review, error) { if _, err := e.Insert(review); err != nil { return nil, err } + + var reviewHookType HookEventType + + switch opts.Type { + case ReviewTypeApprove: + reviewHookType = HookEventPullRequestApproved + case ReviewTypeComment: + reviewHookType = HookEventPullRequestComment + case ReviewTypeReject: + reviewHookType = HookEventPullRequestRejected + default: + // unsupported review webhook type here + return review, nil + } + + pr := opts.Issue.PullRequest + + if err := pr.LoadIssue(); err != nil { + return nil, err + } + + mode, err := AccessLevel(opts.Issue.Poster, opts.Issue.Repo) + if err != nil { + return nil, err + } + + if err := PrepareWebhooks(opts.Issue.Repo, reviewHookType, &api.PullRequestPayload{ + Action: api.HookIssueSynchronized, + Index: opts.Issue.Index, + PullRequest: pr.APIFormat(), + Repository: opts.Issue.Repo.APIFormat(mode), + Sender: opts.Reviewer.APIFormat(), + }); err != nil { + return nil, err + } + go HookQueue.Add(opts.Issue.Repo.ID) + return review, nil } @@ -285,10 +323,10 @@ type PullReviewersWithType struct { func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) { irs := []*PullReviewersWithType{} if x.Dialect().DBType() == core.MSSQL { - err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM -(SELECT review.id, review.type, review.reviewer_id, max(review.updated_unix) as review_updated_unix -FROM review WHERE review.issue_id=? AND (review.type = ? OR review.type = ?) -GROUP BY review.id, review.type, review.reviewer_id) as review + err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM +(SELECT review.id, review.type, review.reviewer_id, max(review.updated_unix) as review_updated_unix +FROM review WHERE review.issue_id=? AND (review.type = ? OR review.type = ?) +GROUP BY review.id, review.type, review.reviewer_id) as review INNER JOIN [user] ON review.reviewer_id = [user].id ORDER BY review_updated_unix DESC`, pullID, ReviewTypeApprove, ReviewTypeReject). Find(&irs) diff --git a/models/webhook.go b/models/webhook.go index 77662f52757a..a764455f5f39 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/util" api "code.gitea.io/sdk/gitea" - "github.com/Unknwon/com" gouuid "github.com/satori/go.uuid" ) @@ -425,15 +424,18 @@ type HookEventType string // Types of hook events const ( - HookEventCreate HookEventType = "create" - HookEventDelete HookEventType = "delete" - HookEventFork HookEventType = "fork" - HookEventPush HookEventType = "push" - HookEventIssues HookEventType = "issues" - HookEventIssueComment HookEventType = "issue_comment" - HookEventPullRequest HookEventType = "pull_request" - HookEventRepository HookEventType = "repository" - HookEventRelease HookEventType = "release" + HookEventCreate HookEventType = "create" + HookEventDelete HookEventType = "delete" + HookEventFork HookEventType = "fork" + HookEventPush HookEventType = "push" + HookEventIssues HookEventType = "issues" + HookEventIssueComment HookEventType = "issue_comment" + HookEventPullRequest HookEventType = "pull_request" + HookEventRepository HookEventType = "repository" + HookEventRelease HookEventType = "release" + HookEventPullRequestApproved HookEventType = "pull_request_approved" + HookEventPullRequestRejected HookEventType = "pull_request_rejected" + HookEventPullRequestComment HookEventType = "pull_request_comment" ) // HookRequest represents hook task request information. diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go index 06388a6ba22b..dbbbebcd9a05 100644 --- a/models/webhook_dingtalk.go +++ b/models/webhook_dingtalk.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/git" api "code.gitea.io/sdk/gitea" - dingtalk "github.com/lunny/dingtalk_webhook" ) @@ -271,6 +270,32 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, }, nil } +func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event HookEventType) (*DingtalkPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title + "\r\n\r\n" + text, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: p.PullRequest.HTMLURL, + }, + }, nil +} + func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) { var title, url string switch p.Action { @@ -369,6 +394,8 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*Din return getDingtalkPushPayload(p.(*api.PushPayload)) case HookEventPullRequest: return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) + case HookEventPullRequestApproved, HookEventPullRequestRejected, HookEventPullRequestComment: + return getDingtalkPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) case HookEventRepository: return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) case HookEventRelease: diff --git a/models/webhook_discord.go b/models/webhook_discord.go index 77634fbe9a8f..4011880ea903 100644 --- a/models/webhook_discord.go +++ b/models/webhook_discord.go @@ -400,6 +400,40 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) }, nil } +func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event HookEventType) (*DiscordPayload, error) { + var text, title string + var color int + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = warnColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: p.PullRequest.HTMLURL, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) { var title, url string var color int @@ -492,6 +526,8 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc return getDiscordPushPayload(p.(*api.PushPayload), discord) case HookEventPullRequest: return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) + case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment: + return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event) case HookEventRepository: return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) case HookEventRelease: @@ -500,3 +536,19 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc return s, nil } + +func parseHookPullRequestEventType(event HookEventType) (string, error) { + + switch event { + + case HookEventPullRequestApproved: + return "approved", nil + case HookEventPullRequestRejected: + return "rejected", nil + case HookEventPullRequestComment: + return "comment", nil + + default: + return "", errors.New("unknown event type") + } +} diff --git a/models/webhook_slack.go b/models/webhook_slack.go index 23df17bf2c72..3a21f9860111 100644 --- a/models/webhook_slack.go +++ b/models/webhook_slack.go @@ -11,9 +11,8 @@ import ( "strings" "code.gitea.io/git" - api "code.gitea.io/sdk/gitea" - "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/sdk/gitea" ) // SlackMeta contains the slack metadata @@ -328,6 +327,34 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S }, nil } +func getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackMeta, event HookEventType) (*SlackPayload, error) { + senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index), + fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)) + var text, title, attachmentText string + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + text = fmt.Sprintf("[%s] Pull request review %s : %s by %s", p.Repository.FullName, action, titleLink, senderLink) + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Title: title, + Text: attachmentText, + }}, + }, nil +} + func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) var text, title, attachmentText string @@ -376,6 +403,8 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP return getSlackPushPayload(p.(*api.PushPayload), slack) case HookEventPullRequest: return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) + case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment: + return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event) case HookEventRepository: return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) case HookEventRelease: diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5013e0a94b04..0835175f16f0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1101,7 +1101,7 @@ settings.event_issue_comment_desc = Issue comment created, edited, or deleted. settings.event_release = Release settings.event_release_desc = Release published, updated or deleted in a repository. settings.event_pull_request = Pull Request -settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared or synchronized. +settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, approved, rejected, review comment, assigned, unassigned, label updated, label cleared or synchronized. settings.event_push = Push settings.event_push_desc = Git push to a repository. settings.event_repository = Repository