New webhook trigger for receiving Pull Request review requests (#24481)

close https://github.com/go-gitea/gitea/issues/16321

Provided a webhook trigger for requesting someone to review the Pull
Request.

Some modifications have been made to the returned `PullRequestPayload`
based on the GitHub webhook settings, including:
- add a description of the current reviewer object as
`RequestedReviewer` .
- setting the action to either **review_requested** or
**review_request_removed** based on the operation.
- adding the `RequestedReviewers` field to the issues_model.PullRequest.
This field will be loaded into the PullRequest through
`LoadRequestedReviewers()` when `ToAPIPullRequest` is called.

After the Pull Request is merged, I will supplement the relevant
documentation.
This commit is contained in:
谈笑风生间 2023-05-25 10:06:27 +08:00 committed by GitHub
parent 93c6a9a652
commit 309354c70e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 305 additions and 117 deletions

View File

@ -175,9 +175,10 @@ type PullRequest struct {
ChangedProtectedFiles []string `xorm:"TEXT JSON"` ChangedProtectedFiles []string `xorm:"TEXT JSON"`
IssueID int64 `xorm:"INDEX"` IssueID int64 `xorm:"INDEX"`
Issue *Issue `xorm:"-"` Issue *Issue `xorm:"-"`
Index int64 Index int64
RequestedReviewers []*user_model.User `xorm:"-"`
HeadRepoID int64 `xorm:"INDEX"` HeadRepoID int64 `xorm:"INDEX"`
HeadRepo *repo_model.Repository `xorm:"-"` HeadRepo *repo_model.Repository `xorm:"-"`
@ -302,6 +303,29 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
return nil return nil
} }
// LoadRequestedReviewers loads the requested reviewers.
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
if len(pr.RequestedReviewers) > 0 {
return nil
}
reviews, err := GetReviewsByIssueID(pr.Issue.ID)
if err != nil {
return err
}
if len(reviews) > 0 {
err = LoadReviewers(ctx, reviews)
if err != nil {
return err
}
for _, review := range reviews {
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
}
}
return nil
}
// LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned. // LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned.
func (pr *PullRequest) LoadBaseRepo(ctx context.Context) (err error) { func (pr *PullRequest) LoadBaseRepo(ctx context.Context) (err error) {
if pr.BaseRepo != nil { if pr.BaseRepo != nil {

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -74,6 +75,34 @@ func TestPullRequestsNewest(t *testing.T) {
} }
} }
func TestLoadRequestedReviewers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
issue := pull.Issue
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
assert.Len(t, pull.RequestedReviewers, 0)
user1, err := user_model.GetUserByID(db.DefaultContext, 1)
assert.NoError(t, err)
comment, err := issues_model.AddReviewRequest(issue, user1, &user_model.User{})
assert.NoError(t, err)
assert.NotNil(t, comment)
assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext))
assert.Len(t, pull.RequestedReviewers, 1)
comment, err = issues_model.RemoveReviewRequest(issue, user1, &user_model.User{})
assert.NoError(t, err)
assert.NotNil(t, comment)
pull.RequestedReviewers = nil
assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext))
assert.Empty(t, pull.RequestedReviewers)
}
func TestPullRequestsOldest(t *testing.T) { func TestPullRequestsOldest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{ prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{

View File

@ -162,6 +162,27 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
return err return err
} }
// LoadReviewers loads reviewers
func LoadReviewers(ctx context.Context, reviews []*Review) (err error) {
reviewerIds := make([]int64, len(reviews))
for i := 0; i < len(reviews); i++ {
reviewerIds[i] = reviews[i].ReviewerID
}
reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds)
if err != nil {
return err
}
userMap := make(map[int64]*user_model.User, len(reviewers))
for _, reviewer := range reviewers {
userMap[reviewer.ID] = reviewer
}
for _, review := range reviews {
review.Reviewer = userMap[review.ReviewerID]
}
return nil
}
// LoadReviewerTeam loads reviewer team // LoadReviewerTeam loads reviewer team
func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) { func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil { if r.ReviewerTeamID == 0 || r.ReviewerTeam != nil {
@ -520,8 +541,8 @@ func GetReviews(ctx context.Context, opts *GetReviewOptions) ([]*Review, error)
return reviews, sess.Find(&reviews) return reviews, sess.Find(&reviews)
} }
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request // GetReviewsByIssueID gets the latest review of each reviewer for a pull request
func GetReviewersByIssueID(issueID int64) ([]*Review, error) { func GetReviewsByIssueID(issueID int64) ([]*Review, error) {
reviews := make([]*Review, 0, 10) reviews := make([]*Review, 0, 10)
sess := db.GetEngine(db.DefaultContext) sess := db.GetEngine(db.DefaultContext)

View File

@ -132,11 +132,22 @@ func TestGetReviewersByIssueID(t *testing.T) {
UpdatedUnix: 946684814, UpdatedUnix: 946684814,
}) })
allReviews, err := issues_model.GetReviewersByIssueID(issue.ID) allReviews, err := issues_model.GetReviewsByIssueID(issue.ID)
for _, reviewer := range allReviews {
assert.NoError(t, reviewer.LoadReviewer(db.DefaultContext))
}
assert.NoError(t, err) assert.NoError(t, err)
for _, review := range allReviews {
assert.NoError(t, review.LoadReviewer(db.DefaultContext))
}
if assert.Len(t, allReviews, 3) {
for i, review := range allReviews {
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
assert.Equal(t, expectedReviews[i].Type, review.Type)
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
}
}
allReviews, err = issues_model.GetReviewsByIssueID(issue.ID)
assert.NoError(t, err)
assert.NoError(t, issues_model.LoadReviewers(db.DefaultContext, allReviews))
if assert.Len(t, allReviews, 3) { if assert.Len(t, allReviews, 3) {
for i, review := range allReviews { for i, review := range allReviews {
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/auth/openid" "code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -910,6 +911,15 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
return u, nil return u, nil
} }
// GetUserByIDs returns the user objects by given IDs if exists.
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
users := make([]*User, 0, len(ids))
err := db.GetEngine(ctx).In("id", ids).
Table("user").
Find(&users)
return users, err
}
// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0 // GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
switch id { switch id {
@ -924,6 +934,25 @@ func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
} }
} }
// GetPossibleUserByIDs returns the users if id > 0 or return system users if id < 0
func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
uniqueIDs := container.SetOf(ids...)
users := make([]*User, 0, len(ids))
_ = uniqueIDs.Remove(0)
if uniqueIDs.Remove(-1) {
users = append(users, NewGhostUser())
}
if uniqueIDs.Remove(ActionsUserID) {
users = append(users, NewActionsUser())
}
res, err := GetUserByIDs(ctx, uniqueIDs.Values())
if err != nil {
return nil, err
}
users = append(users, res...)
return users, nil
}
// GetUserByNameCtx returns user by given name. // GetUserByNameCtx returns user by given name.
func GetUserByName(ctx context.Context, name string) (*User, error) { func GetUserByName(ctx context.Context, name string) (*User, error) {
if len(name) == 0 { if len(name) == 0 {

View File

@ -298,6 +298,12 @@ func (w *Webhook) HasPackageEvent() bool {
(w.ChooseEvents && w.HookEvents.Package) (w.ChooseEvents && w.HookEvents.Package)
} }
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.PullRequestReviewRequest)
}
// EventCheckers returns event checkers // EventCheckers returns event checkers
func (w *Webhook) EventCheckers() []struct { func (w *Webhook) EventCheckers() []struct {
Has func() bool Has func() bool
@ -329,6 +335,7 @@ func (w *Webhook) EventCheckers() []struct {
{w.HasRepositoryEvent, webhook_module.HookEventRepository}, {w.HasRepositoryEvent, webhook_module.HookEventRepository},
{w.HasReleaseEvent, webhook_module.HookEventRelease}, {w.HasReleaseEvent, webhook_module.HookEventRelease},
{w.HasPackageEvent, webhook_module.HookEventPackage}, {w.HasPackageEvent, webhook_module.HookEventPackage},
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
} }
} }

View File

@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) {
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release", "pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
"package", "package", "pull_request_review_request",
}, },
(&Webhook{ (&Webhook{
HookEvent: &webhook_module.HookEvent{SendEverything: true}, HookEvent: &webhook_module.HookEvent{SendEverything: true},

View File

@ -342,6 +342,10 @@ const (
HookIssueDemilestoned HookIssueAction = "demilestoned" HookIssueDemilestoned HookIssueAction = "demilestoned"
// HookIssueReviewed is an issue action for when a pull request is reviewed // HookIssueReviewed is an issue action for when a pull request is reviewed
HookIssueReviewed HookIssueAction = "reviewed" HookIssueReviewed HookIssueAction = "reviewed"
// HookIssueReviewRequested is an issue action for when a reviewer is requested for a pull request.
HookIssueReviewRequested HookIssueAction = "review_requested"
// HookIssueReviewRequestRemoved is an issue action for removing a review request to someone on a pull request.
HookIssueReviewRequestRemoved HookIssueAction = "review_request_removed"
) )
// IssuePayload represents the payload information that is sent along with an issue event. // IssuePayload represents the payload information that is sent along with an issue event.
@ -381,14 +385,15 @@ type ChangesPayload struct {
// PullRequestPayload represents a payload information of pull request event. // PullRequestPayload represents a payload information of pull request event.
type PullRequestPayload struct { type PullRequestPayload struct {
Action HookIssueAction `json:"action"` Action HookIssueAction `json:"action"`
Index int64 `json:"number"` Index int64 `json:"number"`
Changes *ChangesPayload `json:"changes,omitempty"` Changes *ChangesPayload `json:"changes,omitempty"`
PullRequest *PullRequest `json:"pull_request"` PullRequest *PullRequest `json:"pull_request"`
Repository *Repository `json:"repository"` RequestedReviewer *User `json:"requested_reviewer"`
Sender *User `json:"sender"` Repository *Repository `json:"repository"`
CommitID string `json:"commit_id"` Sender *User `json:"sender"`
Review *ReviewPayload `json:"review"` CommitID string `json:"commit_id"`
Review *ReviewPayload `json:"review"`
} }
// JSONPayload FIXME // JSONPayload FIXME

View File

@ -9,19 +9,20 @@ import (
// PullRequest represents a pull request // PullRequest represents a pull request
type PullRequest struct { type PullRequest struct {
ID int64 `json:"id"` ID int64 `json:"id"`
URL string `json:"url"` URL string `json:"url"`
Index int64 `json:"number"` Index int64 `json:"number"`
Poster *User `json:"user"` Poster *User `json:"user"`
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` Body string `json:"body"`
Labels []*Label `json:"labels"` Labels []*Label `json:"labels"`
Milestone *Milestone `json:"milestone"` Milestone *Milestone `json:"milestone"`
Assignee *User `json:"assignee"` Assignee *User `json:"assignee"`
Assignees []*User `json:"assignees"` Assignees []*User `json:"assignees"`
State StateType `json:"state"` RequestedReviewers []*User `json:"requested_reviewers"`
IsLocked bool `json:"is_locked"` State StateType `json:"state"`
Comments int `json:"comments"` IsLocked bool `json:"is_locked"`
Comments int `json:"comments"`
HTMLURL string `json:"html_url"` HTMLURL string `json:"html_url"`
DiffURL string `json:"diff_url"` DiffURL string `json:"diff_url"`

View File

@ -5,26 +5,27 @@ package webhook
// HookEvents is a set of web hook events // HookEvents is a set of web hook events
type HookEvents struct { type HookEvents struct {
Create bool `json:"create"` Create bool `json:"create"`
Delete bool `json:"delete"` Delete bool `json:"delete"`
Fork bool `json:"fork"` Fork bool `json:"fork"`
Issues bool `json:"issues"` Issues bool `json:"issues"`
IssueAssign bool `json:"issue_assign"` IssueAssign bool `json:"issue_assign"`
IssueLabel bool `json:"issue_label"` IssueLabel bool `json:"issue_label"`
IssueMilestone bool `json:"issue_milestone"` IssueMilestone bool `json:"issue_milestone"`
IssueComment bool `json:"issue_comment"` IssueComment bool `json:"issue_comment"`
Push bool `json:"push"` Push bool `json:"push"`
PullRequest bool `json:"pull_request"` PullRequest bool `json:"pull_request"`
PullRequestAssign bool `json:"pull_request_assign"` PullRequestAssign bool `json:"pull_request_assign"`
PullRequestLabel bool `json:"pull_request_label"` PullRequestLabel bool `json:"pull_request_label"`
PullRequestMilestone bool `json:"pull_request_milestone"` PullRequestMilestone bool `json:"pull_request_milestone"`
PullRequestComment bool `json:"pull_request_comment"` PullRequestComment bool `json:"pull_request_comment"`
PullRequestReview bool `json:"pull_request_review"` PullRequestReview bool `json:"pull_request_review"`
PullRequestSync bool `json:"pull_request_sync"` PullRequestSync bool `json:"pull_request_sync"`
Wiki bool `json:"wiki"` PullRequestReviewRequest bool `json:"pull_request_review_request"`
Repository bool `json:"repository"` Wiki bool `json:"wiki"`
Release bool `json:"release"` Repository bool `json:"repository"`
Package bool `json:"package"` Release bool `json:"release"`
Package bool `json:"package"`
} }
// HookEvent represents events that will delivery hook. // HookEvent represents events that will delivery hook.

View File

@ -26,6 +26,7 @@ const (
HookEventPullRequestReviewRejected HookEventType = "pull_request_review_rejected" HookEventPullRequestReviewRejected HookEventType = "pull_request_review_rejected"
HookEventPullRequestReviewComment HookEventType = "pull_request_review_comment" HookEventPullRequestReviewComment HookEventType = "pull_request_review_comment"
HookEventPullRequestSync HookEventType = "pull_request_sync" HookEventPullRequestSync HookEventType = "pull_request_sync"
HookEventPullRequestReviewRequest HookEventType = "pull_request_review_request"
HookEventWiki HookEventType = "wiki" HookEventWiki HookEventType = "wiki"
HookEventRepository HookEventType = "repository" HookEventRepository HookEventType = "repository"
HookEventRelease HookEventType = "release" HookEventRelease HookEventType = "release"
@ -46,7 +47,7 @@ func (h HookEventType) Event() string {
case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone: case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone:
return "issues" return "issues"
case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone, case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone,
HookEventPullRequestSync: HookEventPullRequestSync, HookEventPullRequestReviewRequest:
return "pull_request" return "pull_request"
case HookEventIssueComment, HookEventPullRequestComment: case HookEventIssueComment, HookEventPullRequestComment:
return "issue_comment" return "issue_comment"

View File

@ -2118,6 +2118,8 @@ settings.event_pull_request_review = Pull Request Reviewed
settings.event_pull_request_review_desc = Pull request approved, rejected, or review comment. settings.event_pull_request_review_desc = Pull request approved, rejected, or review comment.
settings.event_pull_request_sync = Pull Request Synchronized settings.event_pull_request_sync = Pull Request Synchronized
settings.event_pull_request_sync_desc = Pull request synchronized. settings.event_pull_request_sync_desc = Pull request synchronized.
settings.event_pull_request_review_request = Pull Request Review Requested
settings.event_pull_request_review_request_desc = Pull request review requested or review request removed.
settings.event_pull_request_approvals = Pull Request Approvals settings.event_pull_request_approvals = Pull Request Approvals
settings.event_pull_request_merge = Pull Request Merge settings.event_pull_request_merge = Pull Request Merge
settings.event_package = Package settings.event_package = Package

View File

@ -179,25 +179,26 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
HookEvent: &webhook_module.HookEvent{ HookEvent: &webhook_module.HookEvent{
ChooseEvents: true, ChooseEvents: true,
HookEvents: webhook_module.HookEvents{ HookEvents: webhook_module.HookEvents{
Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
Issues: issuesHook(form.Events, "issues_only"), Issues: issuesHook(form.Events, "issues_only"),
IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
PullRequest: pullHook(form.Events, "pull_request_only"), PullRequest: pullHook(form.Events, "pull_request_only"),
PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
PullRequestReview: pullHook(form.Events, "pull_request_review"), PullRequestReview: pullHook(form.Events, "pull_request_review"),
PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
}, },
BranchFilter: form.BranchFilter, BranchFilter: form.BranchFilter,
}, },
@ -379,6 +380,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
w.PullRequestReview = pullHook(form.Events, "pull_request_review") w.PullRequestReview = pullHook(form.Events, "pull_request_review")
w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)) w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {

View File

@ -576,7 +576,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
} }
ctx.Data["OriginalReviews"] = originalAuthorReviews ctx.Data["OriginalReviews"] = originalAuthorReviews
reviews, err := issues_model.GetReviewersByIssueID(issue.ID) reviews, err := issues_model.GetReviewsByIssueID(issue.ID)
if err != nil { if err != nil {
ctx.ServerError("GetReviewersByIssueID", err) ctx.ServerError("GetReviewersByIssueID", err)
return return

View File

@ -160,26 +160,27 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
SendEverything: form.SendEverything(), SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(), ChooseEvents: form.ChooseEvents(),
HookEvents: webhook_module.HookEvents{ HookEvents: webhook_module.HookEvents{
Create: form.Create, Create: form.Create,
Delete: form.Delete, Delete: form.Delete,
Fork: form.Fork, Fork: form.Fork,
Issues: form.Issues, Issues: form.Issues,
IssueAssign: form.IssueAssign, IssueAssign: form.IssueAssign,
IssueLabel: form.IssueLabel, IssueLabel: form.IssueLabel,
IssueMilestone: form.IssueMilestone, IssueMilestone: form.IssueMilestone,
IssueComment: form.IssueComment, IssueComment: form.IssueComment,
Release: form.Release, Release: form.Release,
Push: form.Push, Push: form.Push,
PullRequest: form.PullRequest, PullRequest: form.PullRequest,
PullRequestAssign: form.PullRequestAssign, PullRequestAssign: form.PullRequestAssign,
PullRequestLabel: form.PullRequestLabel, PullRequestLabel: form.PullRequestLabel,
PullRequestMilestone: form.PullRequestMilestone, PullRequestMilestone: form.PullRequestMilestone,
PullRequestComment: form.PullRequestComment, PullRequestComment: form.PullRequestComment,
PullRequestReview: form.PullRequestReview, PullRequestReview: form.PullRequestReview,
PullRequestSync: form.PullRequestSync, PullRequestSync: form.PullRequestSync,
Wiki: form.Wiki, PullRequestReviewRequest: form.PullRequestReviewRequest,
Repository: form.Repository, Wiki: form.Wiki,
Package: form.Package, Repository: form.Repository,
Package: form.Package,
}, },
BranchFilter: form.BranchFilter, BranchFilter: form.BranchFilter,
} }

View File

@ -88,6 +88,14 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
}, },
} }
if err = pr.LoadRequestedReviewers(ctx); err != nil {
log.Error("LoadRequestedReviewers[%d]: %v", pr.ID, err)
return nil
}
for _, reviewer := range pr.RequestedReviewers {
apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil))
}
if pr.Issue.ClosedUnix != 0 { if pr.Issue.ClosedUnix != 0 {
apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
} }

View File

@ -228,30 +228,31 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin
// WebhookForm form for changing web hook // WebhookForm form for changing web hook
type WebhookForm struct { type WebhookForm struct {
Events string Events string
Create bool Create bool
Delete bool Delete bool
Fork bool Fork bool
Issues bool Issues bool
IssueAssign bool IssueAssign bool
IssueLabel bool IssueLabel bool
IssueMilestone bool IssueMilestone bool
IssueComment bool IssueComment bool
Release bool Release bool
Push bool Push bool
PullRequest bool PullRequest bool
PullRequestAssign bool PullRequestAssign bool
PullRequestLabel bool PullRequestLabel bool
PullRequestMilestone bool PullRequestMilestone bool
PullRequestComment bool PullRequestComment bool
PullRequestReview bool PullRequestReview bool
PullRequestSync bool PullRequestSync bool
Wiki bool PullRequestReviewRequest bool
Repository bool Wiki bool
Package bool Repository bool
Active bool Package bool
BranchFilter string `binding:"GlobPattern"` Active bool
AuthorizationHeader string BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
} }
// PushOnly if the hook will be triggered when push // PushOnly if the hook will be triggered when push

View File

@ -719,6 +719,34 @@ func (m *webhookNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue
} }
} }
func (m *webhookNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
if !issue.IsPull {
log.Warn("NotifyPullReviewRequest: issue is not a pull request: %v", issue.ID)
return
}
mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests)
if err := issue.LoadPullRequest(ctx); err != nil {
log.Error("LoadPullRequest failed: %v", err)
return
}
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
Repository: convert.ToRepo(ctx, issue.Repo, mode),
Sender: convert.ToUser(ctx, doer, nil),
}
if isRequest {
apiPullRequest.Action = api.HookIssueReviewRequested
} else {
apiPullRequest.Action = api.HookIssueReviewRequestRemoved
}
if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestReviewRequest, apiPullRequest); err != nil {
log.Error("PrepareWebhooks [review_requested: %v]: %v", isRequest, err)
return
}
}
func (m *webhookNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { func (m *webhookNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) {
apiPusher := convert.ToUser(ctx, pusher, nil) apiPusher := convert.ToUser(ctx, pusher, nil)
apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone) apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone)

View File

@ -43,7 +43,7 @@ func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.
case webhook_module.HookEventPush: case webhook_module.HookEventPush:
return s.Push(p.(*api.PushPayload)) return s.Push(p.(*api.PushPayload))
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel, case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync: webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
return s.PullRequest(p.(*api.PullRequestPayload)) return s.PullRequest(p.(*api.PullRequestPayload))
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment: case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
return s.Review(p.(*api.PullRequestPayload), event) return s.Review(p.(*api.PullRequestPayload), event)

View File

@ -238,6 +238,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Pull Request Review Request -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input name="pull_request_review_request" type="checkbox" tabindex="0" {{if .Webhook.PullRequestReviewRequest}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.event_pull_request_review_request"}}</label>
<span class="help">{{.locale.Tr "repo.settings.event_pull_request_review_request_desc"}}</span>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -19934,6 +19934,13 @@
"type": "string", "type": "string",
"x-go-name": "PatchURL" "x-go-name": "PatchURL"
}, },
"requested_reviewers": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
},
"x-go-name": "RequestedReviewers"
},
"state": { "state": {
"$ref": "#/definitions/StateType" "$ref": "#/definitions/StateType"
}, },