mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 20:17:03 +01:00
Webhook for Pull Request approval/rejection (#5027)
This commit is contained in:
parent
8bb0a6f425
commit
945804f800
6 changed files with 168 additions and 20 deletions
|
@ -9,10 +9,11 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"github.com/go-xorm/core"
|
api "code.gitea.io/sdk/gitea"
|
||||||
"github.com/go-xorm/xorm"
|
|
||||||
|
|
||||||
"github.com/go-xorm/builder"
|
"github.com/go-xorm/builder"
|
||||||
|
"github.com/go-xorm/core"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReviewType defines the sort of feedback a review gives
|
// 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 {
|
if _, err := e.Insert(review); err != nil {
|
||||||
return nil, err
|
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
|
return review, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,10 +323,10 @@ type PullReviewersWithType struct {
|
||||||
func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) {
|
func GetReviewersByPullID(pullID int64) (issueReviewers []*PullReviewersWithType, err error) {
|
||||||
irs := []*PullReviewersWithType{}
|
irs := []*PullReviewersWithType{}
|
||||||
if x.Dialect().DBType() == core.MSSQL {
|
if x.Dialect().DBType() == core.MSSQL {
|
||||||
err = x.SQL(`SELECT [user].*, review.type, review.review_updated_unix FROM
|
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
|
(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 = ?)
|
FROM review WHERE review.issue_id=? AND (review.type = ? OR review.type = ?)
|
||||||
GROUP BY review.id, review.type, review.reviewer_id) as review
|
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`,
|
INNER JOIN [user] ON review.reviewer_id = [user].id ORDER BY review_updated_unix DESC`,
|
||||||
pullID, ReviewTypeApprove, ReviewTypeReject).
|
pullID, ReviewTypeApprove, ReviewTypeReject).
|
||||||
Find(&irs)
|
Find(&irs)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/sync"
|
"code.gitea.io/gitea/modules/sync"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
api "code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
gouuid "github.com/satori/go.uuid"
|
gouuid "github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
@ -425,15 +424,18 @@ type HookEventType string
|
||||||
|
|
||||||
// Types of hook events
|
// Types of hook events
|
||||||
const (
|
const (
|
||||||
HookEventCreate HookEventType = "create"
|
HookEventCreate HookEventType = "create"
|
||||||
HookEventDelete HookEventType = "delete"
|
HookEventDelete HookEventType = "delete"
|
||||||
HookEventFork HookEventType = "fork"
|
HookEventFork HookEventType = "fork"
|
||||||
HookEventPush HookEventType = "push"
|
HookEventPush HookEventType = "push"
|
||||||
HookEventIssues HookEventType = "issues"
|
HookEventIssues HookEventType = "issues"
|
||||||
HookEventIssueComment HookEventType = "issue_comment"
|
HookEventIssueComment HookEventType = "issue_comment"
|
||||||
HookEventPullRequest HookEventType = "pull_request"
|
HookEventPullRequest HookEventType = "pull_request"
|
||||||
HookEventRepository HookEventType = "repository"
|
HookEventRepository HookEventType = "repository"
|
||||||
HookEventRelease HookEventType = "release"
|
HookEventRelease HookEventType = "release"
|
||||||
|
HookEventPullRequestApproved HookEventType = "pull_request_approved"
|
||||||
|
HookEventPullRequestRejected HookEventType = "pull_request_rejected"
|
||||||
|
HookEventPullRequestComment HookEventType = "pull_request_comment"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookRequest represents hook task request information.
|
// HookRequest represents hook task request information.
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
api "code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
dingtalk "github.com/lunny/dingtalk_webhook"
|
dingtalk "github.com/lunny/dingtalk_webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -271,6 +270,32 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload,
|
||||||
}, nil
|
}, 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) {
|
func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) {
|
||||||
var title, url string
|
var title, url string
|
||||||
switch p.Action {
|
switch p.Action {
|
||||||
|
@ -369,6 +394,8 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*Din
|
||||||
return getDingtalkPushPayload(p.(*api.PushPayload))
|
return getDingtalkPushPayload(p.(*api.PushPayload))
|
||||||
case HookEventPullRequest:
|
case HookEventPullRequest:
|
||||||
return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
|
return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
|
||||||
|
case HookEventPullRequestApproved, HookEventPullRequestRejected, HookEventPullRequestComment:
|
||||||
|
return getDingtalkPullRequestApprovalPayload(p.(*api.PullRequestPayload), event)
|
||||||
case HookEventRepository:
|
case HookEventRepository:
|
||||||
return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
|
return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
|
||||||
case HookEventRelease:
|
case HookEventRelease:
|
||||||
|
|
|
@ -400,6 +400,40 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta)
|
||||||
}, nil
|
}, 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) {
|
func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) {
|
||||||
var title, url string
|
var title, url string
|
||||||
var color int
|
var color int
|
||||||
|
@ -492,6 +526,8 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc
|
||||||
return getDiscordPushPayload(p.(*api.PushPayload), discord)
|
return getDiscordPushPayload(p.(*api.PushPayload), discord)
|
||||||
case HookEventPullRequest:
|
case HookEventPullRequest:
|
||||||
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
|
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
|
||||||
|
case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment:
|
||||||
|
return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event)
|
||||||
case HookEventRepository:
|
case HookEventRepository:
|
||||||
return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
|
return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
|
||||||
case HookEventRelease:
|
case HookEventRelease:
|
||||||
|
@ -500,3 +536,19 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc
|
||||||
|
|
||||||
return s, nil
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,9 +11,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
api "code.gitea.io/sdk/gitea"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SlackMeta contains the slack metadata
|
// SlackMeta contains the slack metadata
|
||||||
|
@ -328,6 +327,34 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
|
||||||
}, nil
|
}, 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) {
|
func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) {
|
||||||
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
|
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
|
||||||
var text, title, attachmentText string
|
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)
|
return getSlackPushPayload(p.(*api.PushPayload), slack)
|
||||||
case HookEventPullRequest:
|
case HookEventPullRequest:
|
||||||
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
|
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
|
||||||
|
case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment:
|
||||||
|
return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event)
|
||||||
case HookEventRepository:
|
case HookEventRepository:
|
||||||
return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
|
return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
|
||||||
case HookEventRelease:
|
case HookEventRelease:
|
||||||
|
|
|
@ -1101,7 +1101,7 @@ settings.event_issue_comment_desc = Issue comment created, edited, or deleted.
|
||||||
settings.event_release = Release
|
settings.event_release = Release
|
||||||
settings.event_release_desc = Release published, updated or deleted in a repository.
|
settings.event_release_desc = Release published, updated or deleted in a repository.
|
||||||
settings.event_pull_request = Pull Request
|
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 = Push
|
||||||
settings.event_push_desc = Git push to a repository.
|
settings.event_push_desc = Git push to a repository.
|
||||||
settings.event_repository = Repository
|
settings.event_repository = Repository
|
||||||
|
|
Loading…
Reference in a new issue