diff --git a/README.md b/README.md index 3037b668..c4df5c9e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ You can specify which events trigger a notification. They can see: - pipeline - include pipeline - tag - include tag creation - pull_reviews - includes merge request reviews +- merge_request_assigns - includes merge request assignment and unassignment notifications - label:"" - must include "merges" or "issues" in feature list when using a label - Defaults to "merges,issues,tag" diff --git a/server/command.go b/server/command.go index 053e3f03..4ffb6c76 100644 --- a/server/command.go +++ b/server/command.go @@ -33,6 +33,7 @@ const commandHelp = `* |/gitlab connect| - Connect your Mattermost account to yo * pushes - includes pushes * issue_comments - includes new issue comments * merge_request_comments - include new merge-request comments + * merge_request_assigns - includes merge request assignment and unassignment notifications * pipeline - includes pipeline runs * tag - include tag creation * pull_reviews - includes merge request reviews @@ -52,14 +53,14 @@ const commandHelp = `* |/gitlab connect| - Connect your Mattermost account to yo * |*| - or missing defaults to all with SSL verification enabled * *noSSL - all triggers with SSL verification not enabled. * PushEvents - * TagPushEvents - * Comments - * ConfidentialComments + * TagPushEvents + * Comments + * ConfidentialComments * IssuesEvents - * ConfidentialIssuesEvents - * MergeRequestsEvents - * JobEvents - * PipelineEvents + * ConfidentialIssuesEvents + * MergeRequestsEvents + * JobEvents + * PipelineEvents * WikiPageEvents * DeploymentEvents * ReleaseEvents @@ -1103,7 +1104,7 @@ func (p *Plugin) getAutocompleteData(config *configuration) *model.AutocompleteD subscriptionsAdd := model.NewAutocompleteData(commandAdd, "owner[/repo] [features]", "Subscribe the current channel to receive notifications from a project") subscriptionsAdd.AddTextArgument("Project path: includes user or group name with optional slash project name", "owner[/repo]", "") - subscriptionsAdd.AddTextArgument("comma-delimited list of features to subscribe to: issues, confidential_issues, merges, pushes, issue_comments, merge_request_comments, pipeline, tag, pull_reviews, label:, deployments, releases", "[features] (optional)", `/[^,-\s]+(,[^,-\s]+)*/`) + subscriptionsAdd.AddTextArgument("comma-delimited list of features to subscribe to: issues, confidential_issues, merges, pushes, issue_comments, merge_request_comments, merge_request_assigns, pipeline, tag, pull_reviews, label:, deployments, releases", "[features] (optional)", `/[^,-\s]+(,[^,-\s]+)*/`) subscriptions.AddCommand(subscriptionsAdd) subscriptionsDelete := model.NewAutocompleteData(commandDelete, "owner[/repo]", "Unsubscribe the current channel from a repository") diff --git a/server/subscription/subscription.go b/server/subscription/subscription.go index 8d1b9db4..53bd0204 100644 --- a/server/subscription/subscription.go +++ b/server/subscription/subscription.go @@ -23,6 +23,7 @@ var allFeatures = map[string]bool{ "confidential_issues": true, "deployments": true, "releases": true, + "merge_request_assigns": true, // "label:": true,//particular case for label:XXX } @@ -146,3 +147,7 @@ func (s *Subscription) Releases() bool { func (s *Subscription) Deployments() bool { return strings.Contains(s.Features, "deployments") } + +func (s *Subscription) MergeRequestAssigns() bool { + return strings.Contains(s.Features, "merge_request_assigns") +} diff --git a/server/webhook/merge_request.go b/server/webhook/merge_request.go index 3e3877cd..10a563b4 100644 --- a/server/webhook/merge_request.go +++ b/server/webhook/merge_request.go @@ -8,6 +8,8 @@ import ( "fmt" "github.com/xanzy/go-gitlab" + + "github.com/mattermost/mattermost-plugin-gitlab/server/subscription" ) func (w *webhook) HandleMergeRequest(ctx context.Context, event *gitlab.MergeEvent) ([]*HandleWebhook, []string, error) { @@ -59,13 +61,23 @@ func (w *webhook) handleDMMergeRequest(event *gitlab.MergeEvent) ([]*HandleWebho // Handle change in assignees if event.Changes.Assignees.Current != nil || event.Changes.Assignees.Previous != nil { - updatedCurrentUsers := w.calculateUserDiffs(event.Changes.Assignees.Previous, event.Changes.Assignees.Current) + newlyAssigned := w.calculateUserDiffs(event.Changes.Assignees.Previous, event.Changes.Assignees.Current) + newlyUnassigned := w.calculateUserDiffs(event.Changes.Assignees.Current, event.Changes.Assignees.Previous) - if len(updatedCurrentUsers) != 0 { + if len(newlyAssigned) != 0 { message = fmt.Sprintf("[%s](%s) assigned you to merge request [#%d](%s) in [%s](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.IID, event.ObjectAttributes.URL, event.ObjectAttributes.Target.PathWithNamespace, event.Repository.Homepage) handlers = append(handlers, &HandleWebhook{ Message: message, - ToUsers: updatedCurrentUsers, + ToUsers: newlyAssigned, + From: senderGitlabUsername, + }) + } + + if len(newlyUnassigned) != 0 { + message = fmt.Sprintf("[%s](%s) unassigned you from merge request [#%d](%s) in [%s](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.IID, event.ObjectAttributes.URL, event.ObjectAttributes.Target.PathWithNamespace, event.Repository.Homepage) + handlers = append(handlers, &HandleWebhook{ + Message: message, + ToUsers: newlyUnassigned, From: senderGitlabUsername, }) } @@ -139,6 +151,7 @@ func (w *webhook) handleChannelMergeRequest(ctx context.Context, event *gitlab.M res := []*HandleWebhook{} var warnings []string message := "" + var assignMessages []string switch pr.Action { case actionOpen: @@ -153,30 +166,37 @@ func (w *webhook) handleChannelMergeRequest(ctx context.Context, event *gitlab.M message = fmt.Sprintf("[%s](%s) Merge request [!%v %s](%s) was approved by [%s](%s)", repo.PathWithNamespace, repo.WebURL, pr.IID, pr.Title, pr.URL, senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername)) case actionUnapproved: message = fmt.Sprintf("[%s](%s) Merge request [!%v %s](%s) changes were requested by [%s](%s)", repo.PathWithNamespace, repo.WebURL, pr.IID, pr.Title, pr.URL, senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername)) - } - - if len(message) > 0 { - toChannels := make([]string, 0) - namespace, project := normalizeNamespacedProject(repo.PathWithNamespace) - subs := w.gitlabRetreiver.GetSubscribedChannelsForProject( - ctx, namespace, project, - repo.Visibility == gitlab.PublicVisibility, - ) - for _, sub := range subs { - if !sub.Merges() { - continue + case actionUpdate: + if event.Changes.Assignees.Current != nil || event.Changes.Assignees.Previous != nil { + newlyAssigned := w.calculateUserDiffs(event.Changes.Assignees.Previous, event.Changes.Assignees.Current) + newlyUnassigned := w.calculateUserDiffs(event.Changes.Assignees.Current, event.Changes.Assignees.Previous) + + for _, username := range newlyAssigned { + msg := fmt.Sprintf("[%s](%s) Merge request [!%v %s](%s) was assigned to [%s](%s) by [%s](%s)", + repo.PathWithNamespace, repo.WebURL, pr.IID, pr.Title, pr.URL, + username, w.gitlabRetreiver.GetUserURL(username), + senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername)) + assignMessages = append(assignMessages, msg) } - - labels, err := sub.Labels() - if err != nil { - warnings = append(warnings, err.Error()) - } else if len(labels) > 0 && !containsAnyLabel(event.Labels, labels) { - continue + for _, username := range newlyUnassigned { + msg := fmt.Sprintf("[%s](%s) Merge request [!%v %s](%s) was unassigned from [%s](%s) by [%s](%s)", + repo.PathWithNamespace, repo.WebURL, pr.IID, pr.Title, pr.URL, + username, w.gitlabRetreiver.GetUserURL(username), + senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername)) + assignMessages = append(assignMessages, msg) } - - toChannels = append(toChannels, sub.ChannelID) } + } + + namespace, project := normalizeNamespacedProject(repo.PathWithNamespace) + subs := w.gitlabRetreiver.GetSubscribedChannelsForProject( + ctx, namespace, project, + repo.Visibility == gitlab.PublicVisibility, + ) + if len(message) > 0 { + toChannels, ws := filterChannelsByFeature(subs, event.Labels, (*subscription.Subscription).Merges) + warnings = append(warnings, ws...) if len(toChannels) > 0 { res = append(res, &HandleWebhook{ From: senderGitlabUsername, @@ -187,6 +207,23 @@ func (w *webhook) handleChannelMergeRequest(ctx context.Context, event *gitlab.M } } + if len(assignMessages) > 0 { + toChannels, ws := filterChannelsByFeature(subs, event.Labels, func(sub *subscription.Subscription) bool { + return sub.Merges() || sub.MergeRequestAssigns() + }) + warnings = append(warnings, ws...) + if len(toChannels) > 0 { + for _, msg := range assignMessages { + res = append(res, &HandleWebhook{ + From: senderGitlabUsername, + Message: msg, + ToUsers: []string{}, + ToChannels: toChannels, + }) + } + } + } + return res, warnings, nil } diff --git a/server/webhook/merge_request_fixture_test.go b/server/webhook/merge_request_fixture_test.go index 15ff76e1..71c700ff 100644 --- a/server/webhook/merge_request_fixture_test.go +++ b/server/webhook/merge_request_fixture_test.go @@ -920,6 +920,178 @@ const RootUpdateReviewerMergeRequest = `{ } }` +const RootUnassignUserMergeRequest = `{ + "object_kind":"merge_request", + "event_type":"merge_request", + "user":{ + "name":"Administrator", + "username":"root", + "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project":{ + "id":24, + "name":"webhook", + "namespace":"manland", + "visibility_level":20, + "path_with_namespace":"manland/webhook", + "web_url":"http://localhost:3000/manland/webhook", + "http_url":"http://localhost:3000/manland/webhook.git" + }, + "object_attributes":{ + "assignee_id":null, + "author_id":1, + "created_at":"2019-04-03 21:07:32 UTC", + "description":"test open merge request", + "id":35, + "iid":4, + "merge_status":"can_be_merged", + "state":"opened", + "title":"Master-2", + "url":"http://localhost:3000/manland/webhook/merge_requests/4", + "source":{ + "id":25, + "name":"webhook", + "namespace":"root", + "visibility_level":20, + "path_with_namespace":"root/webhook", + "http_url":"http://localhost:3000/root/webhook.git" + }, + "target":{ + "id":24, + "name":"webhook", + "namespace":"manland", + "visibility_level":20, + "path_with_namespace":"manland/webhook", + "http_url":"http://localhost:3000/manland/webhook.git" + }, + "last_commit":{ + "id":"1fd967c14f8265a6056525c343d984ce56472d5c", + "message":"Update README.md", + "timestamp":"2019-04-03T21:04:58Z", + "url":"http://localhost:3000/manland/webhook/commit/1fd967c14f8265a6056525c343d984ce56472d5c", + "author":{ + "name":"Administrator", + "email":"admin@example.com" + } + }, + "assignee_ids": [], + "action":"update" + }, + "changes":{ + "assignees":{ + "previous":[ + { + "id": 50, + "name": "manland", + "username": "manland", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + } + ], + "current":[] + } + }, + "assignees": [], + "repository":{ + "name":"webhook", + "url":"ssh://rmaneschi@localhost:2222/manland/webhook.git", + "description":"", + "homepage":"http://localhost:3000/manland/webhook" + } + }` + +const RootAssignMergeRequestWithChannel = `{ + "object_kind":"merge_request", + "event_type":"merge_request", + "user":{ + "name":"Administrator", + "username":"root", + "avatar_url":"https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project":{ + "id":24, + "name":"webhook", + "namespace":"manland", + "visibility_level":20, + "path_with_namespace":"manland/webhook", + "web_url":"http://localhost:3000/manland/webhook", + "http_url":"http://localhost:3000/manland/webhook.git" + }, + "object_attributes":{ + "assignee_id":50, + "author_id":1, + "created_at":"2019-04-03 21:07:32 UTC", + "description":"test open merge request", + "id":35, + "iid":4, + "merge_status":"can_be_merged", + "state":"opened", + "title":"Master-2", + "url":"http://localhost:3000/manland/webhook/merge_requests/4", + "source":{ + "id":25, + "name":"webhook", + "namespace":"root", + "visibility_level":20, + "path_with_namespace":"root/webhook", + "http_url":"http://localhost:3000/root/webhook.git" + }, + "target":{ + "id":24, + "name":"webhook", + "namespace":"manland", + "visibility_level":20, + "path_with_namespace":"manland/webhook", + "http_url":"http://localhost:3000/manland/webhook.git" + }, + "last_commit":{ + "id":"1fd967c14f8265a6056525c343d984ce56472d5c", + "message":"Update README.md", + "timestamp":"2019-04-03T21:04:58Z", + "url":"http://localhost:3000/manland/webhook/commit/1fd967c14f8265a6056525c343d984ce56472d5c", + "author":{ + "name":"Administrator", + "email":"admin@example.com" + } + }, + "assignee_ids": [50], + "action":"update" + }, + "changes":{ + "assignees":{ + "previous":[ + { + "id": 100, + "name": "user", + "username": "user", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + } + ], + "current":[ + { + "id": 50, + "name": "manland", + "username": "manland", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + } + ] + } + }, + "assignees": [ + { + "id": 50, + "name": "manland", + "username": "manland", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + } + ], + "repository":{ + "name":"webhook", + "url":"ssh://rmaneschi@localhost:2222/manland/webhook.git", + "description":"", + "homepage":"http://localhost:3000/manland/webhook" + } + }` + const MultipleEventsMergeRequest = `{ "object_kind":"merge_request", "event_type":"merge_request", diff --git a/server/webhook/merge_request_test.go b/server/webhook/merge_request_test.go index 98131b0c..d291bcb6 100644 --- a/server/webhook/merge_request_test.go +++ b/server/webhook/merge_request_test.go @@ -111,6 +111,12 @@ var testDataMergeRequest = []testDataMergeRequestStr{ ToChannels: []string{}, From: "root", }, + { + Message: "[root](http://my.gitlab.com/root) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"user"}, + ToChannels: []string{}, + From: "root", + }, }, warnings: []string{}, }, @@ -139,6 +145,12 @@ var testDataMergeRequest = []testDataMergeRequestStr{ ToChannels: []string{}, From: "user", }, + { + Message: "[user](http://my.gitlab.com/user) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"root"}, + ToChannels: []string{}, + From: "user", + }, }, warnings: []string{}, }, @@ -153,6 +165,116 @@ var testDataMergeRequest = []testDataMergeRequestStr{ ToChannels: []string{}, From: "user", }, + { + Message: "[user](http://my.gitlab.com/user) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"manland"}, + ToChannels: []string{}, + From: "user", + }, + }, + warnings: []string{}, + }, + { + testTitle: "root unassign manland from the merge-request", + fixture: RootUnassignUserMergeRequest, + gitlabRetreiver: newFakeWebhook([]*subscription.Subscription{}), + res: []*HandleWebhook{ + { + Message: "[root](http://my.gitlab.com/root) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"manland"}, + ToChannels: []string{}, + From: "root", + }, + }, + warnings: []string{}, + }, + { + testTitle: "root assign manland to merge-request and display in channel1 with merges subscription", + fixture: RootAssignMergeRequestWithChannel, + gitlabRetreiver: newFakeWebhook([]*subscription.Subscription{ + {ChannelID: "channel1", CreatorID: "1", Features: "merges", Repository: "manland/webhook"}, + }), + res: []*HandleWebhook{ + { + Message: "[root](http://my.gitlab.com/root) assigned you to merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"manland"}, + ToChannels: []string{}, + From: "root", + }, + { + Message: "[root](http://my.gitlab.com/root) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"user"}, + ToChannels: []string{}, + From: "root", + }, + { + Message: "[manland/webhook](http://localhost:3000/manland/webhook) Merge request [!4 Master-2](http://localhost:3000/manland/webhook/merge_requests/4) was assigned to [manland](http://my.gitlab.com/manland) by [root](http://my.gitlab.com/root)", + ToUsers: []string{}, + ToChannels: []string{"channel1"}, + From: "root", + }, + { + Message: "[manland/webhook](http://localhost:3000/manland/webhook) Merge request [!4 Master-2](http://localhost:3000/manland/webhook/merge_requests/4) was unassigned from [user](http://my.gitlab.com/user) by [root](http://my.gitlab.com/root)", + ToUsers: []string{}, + ToChannels: []string{"channel1"}, + From: "root", + }, + }, + warnings: []string{}, + }, + { + testTitle: "root assign manland to merge-request and display in channel1 with merge_request_assigns subscription", + fixture: RootAssignMergeRequestWithChannel, + gitlabRetreiver: newFakeWebhook([]*subscription.Subscription{ + {ChannelID: "channel1", CreatorID: "1", Features: "merge_request_assigns", Repository: "manland/webhook"}, + }), + res: []*HandleWebhook{ + { + Message: "[root](http://my.gitlab.com/root) assigned you to merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"manland"}, + ToChannels: []string{}, + From: "root", + }, + { + Message: "[root](http://my.gitlab.com/root) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"user"}, + ToChannels: []string{}, + From: "root", + }, + { + Message: "[manland/webhook](http://localhost:3000/manland/webhook) Merge request [!4 Master-2](http://localhost:3000/manland/webhook/merge_requests/4) was assigned to [manland](http://my.gitlab.com/manland) by [root](http://my.gitlab.com/root)", + ToUsers: []string{}, + ToChannels: []string{"channel1"}, + From: "root", + }, + { + Message: "[manland/webhook](http://localhost:3000/manland/webhook) Merge request [!4 Master-2](http://localhost:3000/manland/webhook/merge_requests/4) was unassigned from [user](http://my.gitlab.com/user) by [root](http://my.gitlab.com/root)", + ToUsers: []string{}, + ToChannels: []string{"channel1"}, + From: "root", + }, + }, + warnings: []string{}, + }, + { + testTitle: "root assign manland to merge-request but no channel notification without matching subscription", + fixture: RootAssignMergeRequestWithChannel, + gitlabRetreiver: newFakeWebhook([]*subscription.Subscription{ + {ChannelID: "channel1", CreatorID: "1", Features: "issues", Repository: "manland/webhook"}, + }), + res: []*HandleWebhook{ + { + Message: "[root](http://my.gitlab.com/root) assigned you to merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"manland"}, + ToChannels: []string{}, + From: "root", + }, + { + Message: "[root](http://my.gitlab.com/root) unassigned you from merge request [#4](http://localhost:3000/manland/webhook/merge_requests/4) in [manland/webhook](http://localhost:3000/manland/webhook)", + ToUsers: []string{"user"}, + ToChannels: []string{}, + From: "root", + }, }, warnings: []string{}, }, diff --git a/server/webhook/webhook.go b/server/webhook/webhook.go index 058712ae..de386ac5 100644 --- a/server/webhook/webhook.go +++ b/server/webhook/webhook.go @@ -149,6 +149,30 @@ func containsAnyLabel(a []*gitlab.EventLabel, labels []string) bool { return false } +func filterChannelsByFeature( + subs []*subscription.Subscription, + eventLabels []*gitlab.EventLabel, + featureCheck func(*subscription.Subscription) bool, +) ([]string, []string) { + var channels []string + var warnings []string + for _, sub := range subs { + if !featureCheck(sub) { + continue + } + + labels, err := sub.Labels() + if err != nil { + warnings = append(warnings, err.Error()) + } else if len(labels) > 0 && !containsAnyLabel(eventLabels, labels) { + continue + } + + channels = append(channels, sub.ChannelID) + } + return channels, warnings +} + func anyEventLabelInSubs(sub *subscription.Subscription, eventLabels []*gitlab.EventLabel) (bool, string) { labels, err := sub.Labels() var warning string