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] Issue webhook #908

Closed
wants to merge 10 commits into from
Closed
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
126 changes: 58 additions & 68 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,12 @@ func (issue *Issue) HasLabel(labelID int64) bool {

func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
var err error
base := newBaseIssueHook(api.HookIssueLabelUpdated, issue, doer)
if issue.IsPull {
err = issue.PullRequest.LoadIssue()
if err != nil {
log.Error(4, "LoadIssue: %v", err)
return
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueLabelUpdated,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
})
issue.PullRequest.Issue = issue
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
Expand Down Expand Up @@ -422,19 +415,16 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
return fmt.Errorf("Commit: %v", err)
}

base := newBaseIssueHook(api.HookIssueLabelCleared, issue, doer)
if issue.IsPull {
err = issue.PullRequest.LoadIssue()
issue.PullRequest.Issue = issue
if err != nil {
log.Error(4, "LoadIssue: %v", err)
return
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueLabelCleared,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
})
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
Expand Down Expand Up @@ -613,22 +603,19 @@ func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (e
return fmt.Errorf("Commit: %v", err)
}

// Prepare webhook
base := newBaseIssueHook(api.HookIssueReOpened, issue, doer)
if isClosed {
base.Action = api.HookIssueClosed
}
if issue.IsPull {
// Merge pull request calls issue.changeStatus so we need to handle separately.
issue.PullRequest.Issue = issue
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
}
if isClosed {
apiPullRequest.Action = api.HookIssueClosed
} else {
apiPullRequest.Action = api.HookIssueReOpened
}
err = PrepareWebhooks(repo, HookEventPullRequest, apiPullRequest)
err = PrepareWebhooks(repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}

if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err)
} else {
Expand Down Expand Up @@ -661,20 +648,17 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
return err
}

base := newBaseIssueHook(api.HookIssueEdited, issue, doer)
base.Changes = &api.ChangesPayload{
Title: &api.ChangesFromPayload{
From: oldTitle,
},
}
if issue.IsPull {
issue.PullRequest.Issue = issue
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueEdited,
Index: issue.Index,
Changes: &api.ChangesPayload{
Title: &api.ChangesFromPayload{
From: oldTitle,
},
},
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
})
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
Expand All @@ -693,20 +677,17 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
return fmt.Errorf("UpdateIssueCols: %v", err)
}

base := newBaseIssueHook(api.HookIssueEdited, issue, doer)
base.Changes = &api.ChangesPayload{
Body: &api.ChangesFromPayload{
From: oldContent,
},
}
if issue.IsPull {
issue.PullRequest.Issue = issue
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: api.HookIssueEdited,
Index: issue.Index,
Changes: &api.ChangesPayload{
Body: &api.ChangesFromPayload{
From: oldContent,
},
},
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
})
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
Expand Down Expand Up @@ -744,20 +725,16 @@ func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {

// Error not nil here means user does not exist, which is remove assignee.
isRemoveAssignee := err != nil

base := newBaseIssueHook(api.HookIssueAssigned, issue, doer)
if isRemoveAssignee {
base.Action = api.HookIssueUnassigned
}
if issue.IsPull {
issue.PullRequest.Issue = issue
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
}
if isRemoveAssignee {
apiPullRequest.Action = api.HookIssueUnassigned
} else {
apiPullRequest.Action = api.HookIssueAssigned
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest)
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
Expand Down Expand Up @@ -908,6 +885,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
return fmt.Errorf("newIssue: %v", err)
}

// We set Created and Updated so that hooks don't receive a zero-value
// created_at and updated_at.
issue.Created = time.Now().Round(time.Second)
issue.Updated = issue.Created

if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
}
Expand All @@ -928,6 +910,14 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
log.Error(4, "MailParticipants: %v", err)
}

if err = PrepareWebhooks(
repo, HookEventIssue,
newBaseIssueHook(api.HookIssueOpened, issue, issue.Poster).ToIssuePayload(issue.APIFormat()),
); err != nil {
log.Error(4, "PrepareWebhooks: %v", err)
}
go HookQueue.Add(repo.ID)

return nil
}

Expand Down
18 changes: 18 additions & 0 deletions models/issue_milestone.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/sdk/gitea"

"code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm"
)

Expand Down Expand Up @@ -311,6 +312,23 @@ func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err
if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
return err
}

base := newBaseIssueHook(api.HookIssueMilestoned, issue, doer)
if issue.MilestoneID == 0 {
base.Action = api.HookIssueDemilestoned
}
if issue.IsPull {
issue.PullRequest.Issue = issue
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, base.ToPRPayload(issue.PullRequest.APIFormat()))
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssue, base.ToIssuePayload(issue.APIFormat()))
}
if err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
} else {
go HookQueue.Add(issue.RepoID)
}

return sess.Commit()
}

Expand Down
4 changes: 3 additions & 1 deletion models/issue_milestone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (

api "code.gitea.io/sdk/gitea"

"github.com/stretchr/testify/assert"
"sort"
"time"

"github.com/stretchr/testify/assert"
)

func TestMilestone_State(t *testing.T) {
Expand Down Expand Up @@ -217,6 +218,7 @@ func TestChangeMilestoneAssign(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue)
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
issue.Poster = doer

oldMilestoneID := issue.MilestoneID
issue.MilestoneID = 2
Expand Down
63 changes: 62 additions & 1 deletion models/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func IsValidHookContentType(name string) bool {
type HookEvents struct {
Create bool `json:"create"`
Push bool `json:"push"`
Issue bool `json:"issue"`
PullRequest bool `json:"pull_request"`
}

Expand Down Expand Up @@ -173,6 +174,12 @@ func (w *Webhook) HasPushEvent() bool {
(w.ChooseEvents && w.HookEvents.Push)
}

// HasIssueEvent returns true if hook enabled issue event.
func (w *Webhook) HasIssueEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Issue)
}

// HasPullRequestEvent returns true if hook enabled pull request event.
func (w *Webhook) HasPullRequestEvent() bool {
return w.SendEverything ||
Expand All @@ -181,13 +188,16 @@ func (w *Webhook) HasPullRequestEvent() bool {

// EventsArray returns an array of hook events
func (w *Webhook) EventsArray() []string {
events := make([]string, 0, 3)
events := make([]string, 0, 4)
if w.HasCreateEvent() {
events = append(events, "create")
}
if w.HasPushEvent() {
events = append(events, "push")
}
if w.HasIssueEvent() {
events = append(events, "issue")
}
if w.HasPullRequestEvent() {
events = append(events, "pull_request")
}
Expand Down Expand Up @@ -348,6 +358,7 @@ type HookEventType string
const (
HookEventCreate HookEventType = "create"
HookEventPush HookEventType = "push"
HookEventIssue HookEventType = "issue"
HookEventPullRequest HookEventType = "pull_request"
)

Expand Down Expand Up @@ -497,6 +508,10 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
if !w.HasPushEvent() {
continue
}
case HookEventIssue:
if !w.HasIssueEvent() {
continue
}
case HookEventPullRequest:
if !w.HasPullRequestEvent() {
continue
Expand Down Expand Up @@ -653,3 +668,49 @@ func DeliverHooks() {
func InitDeliverHooks() {
go DeliverHooks()
}

// baseIssueHook contains the information that is passed for webhooks of both
// issues and pull requests.
type baseIssueHook struct {
Secret string
Action api.HookIssueAction
Index int64
Changes *api.ChangesPayload
Repository *api.Repository
Sender *api.User
}

func newBaseIssueHook(act api.HookIssueAction, issue *Issue, doer *User) *baseIssueHook {
return &baseIssueHook{
Action: act,
Index: issue.Index,
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
}
}

// ToPRPayload converts a baseIssueHook to a PullRequestPayload.
func (b *baseIssueHook) ToPRPayload(pr *api.PullRequest) *api.PullRequestPayload {
return &api.PullRequestPayload{
Secret: b.Secret,
Action: b.Action,
Index: b.Index,
Changes: b.Changes,
Repository: b.Repository,
Sender: b.Sender,
PullRequest: pr,
}
}

// ToIssuePayload converts a baseIssueHook to an IssuePayload.
func (b *baseIssueHook) ToIssuePayload(i *api.Issue) *api.IssuePayload {
return &api.IssuePayload{
Secret: b.Secret,
Action: b.Action,
Index: b.Index,
Changes: b.Changes,
Repository: b.Repository,
Sender: b.Sender,
Issue: i,
}
}
2 changes: 1 addition & 1 deletion models/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestWebhook_UpdateEvent(t *testing.T) {
}

func TestWebhook_EventsArray(t *testing.T) {
assert.Equal(t, []string{"create", "push", "pull_request"},
assert.Equal(t, []string{"create", "push", "issue", "pull_request"},
(&Webhook{
HookEvent: &HookEvent{SendEverything: true},
}).EventsArray(),
Expand Down
1 change: 1 addition & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type WebhookForm struct {
Events string
Create bool
Push bool
Issue bool
PullRequest bool
Active bool
}
Expand Down
4 changes: 3 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,9 @@ settings.event_choose = Let me choose what I need.
settings.event_create = Create
settings.event_create_desc = Branch, or tag created
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, assigned, unassigned, label updated, label cleared, milestoned, demilestoned or synchronized.
settings.event_issue = Issue
settings.event_issue_desc = Issue opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, milestoned or demilestoned.
settings.event_push = Push
settings.event_push_desc = Git push to a repository
settings.active = Active
Expand Down
1 change: 1 addition & 0 deletions routers/repo/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
HookEvents: models.HookEvents{
Create: form.Create,
Push: form.Push,
Issue: form.Issue,
PullRequest: form.PullRequest,
},
}
Expand Down
Loading