##// END OF EJS Templates
events: added support for pull-request-comment and commit-comment events....
marcink -
r4314:9d74b996 default
parent child Browse files
Show More
@@ -0,0 +1,26 b''
1 <div tal:define="css_class css_class|field.widget.css_class;
2 style style|field.widget.style;
3 oid oid|field.oid;
4 inline getattr(field.widget, 'inline', False)"
5 tal:omit-tag="not inline">
6 ${field.start_sequence()}
7 <div tal:repeat="choice values | field.widget.values"
8 tal:omit-tag="inline"
9 class="checkbox">
10 <div tal:define="(value, title, help_block) choice">
11 <input tal:attributes="checked value in cstruct;
12 class css_class;
13 style style"
14 type="checkbox"
15 name="checkbox"
16 value="${value}"
17 id="${oid}-${repeat.choice.index}"/>
18 <label for="${oid}-${repeat.choice.index}"
19 tal:attributes="class inline and 'checkbox-inline'">
20 ${title}
21 </label>
22 <p tal:condition="help_block" class="help-block">${help_block}</p>
23 </div>
24 </div>
25 ${field.end_sequence()}
26 </div>
@@ -1,78 +1,79 b''
1 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 from pyramid.threadlocal import get_current_registry
20 from pyramid.threadlocal import get_current_registry
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
22
22
23
23
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 def trigger(event, registry=None):
27 def trigger(event, registry=None):
28 """
28 """
29 Helper method to send an event. This wraps the pyramid logic to send an
29 Helper method to send an event. This wraps the pyramid logic to send an
30 event.
30 event.
31 """
31 """
32 # For the first step we are using pyramids thread locals here. If the
32 # For the first step we are using pyramids thread locals here. If the
33 # event mechanism works out as a good solution we should think about
33 # event mechanism works out as a good solution we should think about
34 # passing the registry as an argument to get rid of it.
34 # passing the registry as an argument to get rid of it.
35 event_name = event.__class__
35 event_name = event.__class__
36 log.debug('event %s sent for execution', event_name)
36 log.debug('event %s sent for execution', event_name)
37 registry = registry or get_current_registry()
37 registry = registry or get_current_registry()
38 registry.notify(event)
38 registry.notify(event)
39 log.debug('event %s triggered using registry %s', event_name, registry)
39 log.debug('event %s triggered using registry %s', event_name, registry)
40
40
41 # Send the events to integrations directly
41 # Send the events to integrations directly
42 from rhodecode.integrations import integrations_event_handler
42 from rhodecode.integrations import integrations_event_handler
43 if isinstance(event, RhodeCodeIntegrationEvent):
43 if isinstance(event, RhodeCodeIntegrationEvent):
44 integrations_event_handler(event)
44 integrations_event_handler(event)
45
45
46
46
47 from rhodecode.events.user import ( # pragma: no cover
47 from rhodecode.events.user import ( # pragma: no cover
48 UserPreCreate,
48 UserPreCreate,
49 UserPostCreate,
49 UserPostCreate,
50 UserPreUpdate,
50 UserPreUpdate,
51 UserRegistered,
51 UserRegistered,
52 UserPermissionsChange,
52 UserPermissionsChange,
53 )
53 )
54
54
55 from rhodecode.events.repo import ( # pragma: no cover
55 from rhodecode.events.repo import ( # pragma: no cover
56 RepoEvent, RepoCommitCommentEvent,
56 RepoEvent, RepoCommitCommentEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
59 RepoPrePushEvent, RepoPushEvent,
59 RepoPrePushEvent, RepoPushEvent,
60 RepoPrePullEvent, RepoPullEvent,
60 RepoPrePullEvent, RepoPullEvent,
61 )
61 )
62
62
63 from rhodecode.events.repo_group import ( # pragma: no cover
63 from rhodecode.events.repo_group import ( # pragma: no cover
64 RepoGroupEvent,
64 RepoGroupEvent,
65 RepoGroupCreateEvent,
65 RepoGroupCreateEvent,
66 RepoGroupUpdateEvent,
66 RepoGroupUpdateEvent,
67 RepoGroupDeleteEvent,
67 RepoGroupDeleteEvent,
68 )
68 )
69
69
70 from rhodecode.events.pullrequest import ( # pragma: no cover
70 from rhodecode.events.pullrequest import ( # pragma: no cover
71 PullRequestEvent,
71 PullRequestEvent,
72 PullRequestCreateEvent,
72 PullRequestCreateEvent,
73 PullRequestUpdateEvent,
73 PullRequestUpdateEvent,
74 PullRequestCommentEvent,
74 PullRequestCommentEvent,
75 PullRequestReviewEvent,
75 PullRequestReviewEvent,
76 PullRequestMergeEvent,
76 PullRequestMergeEvent,
77 PullRequestCloseEvent,
77 PullRequestCloseEvent,
78 PullRequestCommentEvent,
78 )
79 )
@@ -1,118 +1,122 b''
1 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import logging
18 import logging
19 import datetime
19 import datetime
20
20
21 from zope.cachedescriptors.property import Lazy as LazyProperty
21 from zope.cachedescriptors.property import Lazy as LazyProperty
22 from pyramid.threadlocal import get_current_request
22 from pyramid.threadlocal import get_current_request
23
23
24 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
25
25
26
26
27 # this is a user object to be used for events caused by the system (eg. shell)
27 # this is a user object to be used for events caused by the system (eg. shell)
28 SYSTEM_USER = AttributeDict(dict(
28 SYSTEM_USER = AttributeDict(dict(
29 username='__SYSTEM__',
29 username='__SYSTEM__',
30 user_id='__SYSTEM_ID__'
30 user_id='__SYSTEM_ID__'
31 ))
31 ))
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class RhodecodeEvent(object):
36 class RhodecodeEvent(object):
37 """
37 """
38 Base event class for all RhodeCode events
38 Base event class for all RhodeCode events
39 """
39 """
40 name = "RhodeCodeEvent"
40 name = "RhodeCodeEvent"
41 no_url_set = '<no server_url available>'
41 no_url_set = '<no server_url available>'
42
42
43 def __init__(self, request=None):
43 def __init__(self, request=None):
44 self._request = request
44 self._request = request
45 self.utc_timestamp = datetime.datetime.utcnow()
45 self.utc_timestamp = datetime.datetime.utcnow()
46
46
47 def __repr__(self):
48 return '<%s:(%s)>' % (self.__class__.__name__, self.name)
49
47 def get_request(self):
50 def get_request(self):
48 if self._request:
51 if self._request:
49 return self._request
52 return self._request
50 return get_current_request()
53 return get_current_request()
51
54
52 @LazyProperty
55 @LazyProperty
53 def request(self):
56 def request(self):
54 return self.get_request()
57 return self.get_request()
55
58
56 @property
59 @property
57 def auth_user(self):
60 def auth_user(self):
58 if not self.request:
61 if not self.request:
59 return
62 return
60
63
61 user = getattr(self.request, 'user', None)
64 user = getattr(self.request, 'user', None)
62 if user:
65 if user:
63 return user
66 return user
64
67
65 api_user = getattr(self.request, 'rpc_user', None)
68 api_user = getattr(self.request, 'rpc_user', None)
66 if api_user:
69 if api_user:
67 return api_user
70 return api_user
68
71
69 @property
72 @property
70 def actor(self):
73 def actor(self):
71 auth_user = self.auth_user
74 auth_user = self.auth_user
72 if auth_user:
75 if auth_user:
73 instance = auth_user.get_instance()
76 instance = auth_user.get_instance()
74 if not instance:
77 if not instance:
75 return AttributeDict(dict(
78 return AttributeDict(dict(
76 username=auth_user.username,
79 username=auth_user.username,
77 user_id=auth_user.user_id,
80 user_id=auth_user.user_id,
78 ))
81 ))
79 return instance
82 return instance
80
83
81 return SYSTEM_USER
84 return SYSTEM_USER
82
85
83 @property
86 @property
84 def actor_ip(self):
87 def actor_ip(self):
85 auth_user = self.auth_user
88 auth_user = self.auth_user
86 if auth_user:
89 if auth_user:
87 return auth_user.ip_addr
90 return auth_user.ip_addr
88 return '<no ip available>'
91 return '<no ip available>'
89
92
90 @property
93 @property
91 def server_url(self):
94 def server_url(self):
92 if self.request:
95 if self.request:
93 try:
96 try:
94 return self.request.route_url('home')
97 return self.request.route_url('home')
95 except Exception:
98 except Exception:
96 log.exception('Failed to fetch URL for server')
99 log.exception('Failed to fetch URL for server')
97 return self.no_url_set
100 return self.no_url_set
98
101
99 return self.no_url_set
102 return self.no_url_set
100
103
101 def as_dict(self):
104 def as_dict(self):
102 data = {
105 data = {
103 'name': self.name,
106 'name': self.name,
104 'utc_timestamp': self.utc_timestamp,
107 'utc_timestamp': self.utc_timestamp,
105 'actor_ip': self.actor_ip,
108 'actor_ip': self.actor_ip,
106 'actor': {
109 'actor': {
107 'username': self.actor.username,
110 'username': self.actor.username,
108 'user_id': self.actor.user_id
111 'user_id': self.actor.user_id
109 },
112 },
110 'server_url': self.server_url
113 'server_url': self.server_url
111 }
114 }
112 return data
115 return data
113
116
114
117
115 class RhodeCodeIntegrationEvent(RhodecodeEvent):
118 class RhodeCodeIntegrationEvent(RhodecodeEvent):
116 """
119 """
117 Special subclass for Integration events
120 Special subclass for Integration events
118 """
121 """
122 description = ''
@@ -1,155 +1,164 b''
1 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20
20
21 from rhodecode.translation import lazy_ugettext
21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.repo import (
22 from rhodecode.events.repo import (
23 RepoEvent, _commits_as_dict, _issues_as_dict)
23 RepoEvent, _commits_as_dict, _issues_as_dict)
24
24
25 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
26
26
27
27
28 class PullRequestEvent(RepoEvent):
28 class PullRequestEvent(RepoEvent):
29 """
29 """
30 Base class for pull request events.
30 Base class for pull request events.
31
31
32 :param pullrequest: a :class:`PullRequest` instance
32 :param pullrequest: a :class:`PullRequest` instance
33 """
33 """
34
34
35 def __init__(self, pullrequest):
35 def __init__(self, pullrequest):
36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
37 self.pullrequest = pullrequest
37 self.pullrequest = pullrequest
38
38
39 def as_dict(self):
39 def as_dict(self):
40 from rhodecode.lib.utils2 import md5_safe
40 from rhodecode.lib.utils2 import md5_safe
41 from rhodecode.model.pull_request import PullRequestModel
41 from rhodecode.model.pull_request import PullRequestModel
42 data = super(PullRequestEvent, self).as_dict()
42 data = super(PullRequestEvent, self).as_dict()
43
43
44 commits = _commits_as_dict(
44 commits = _commits_as_dict(
45 self,
45 self,
46 commit_ids=self.pullrequest.revisions,
46 commit_ids=self.pullrequest.revisions,
47 repos=[self.pullrequest.source_repo]
47 repos=[self.pullrequest.source_repo]
48 )
48 )
49 issues = _issues_as_dict(commits)
49 issues = _issues_as_dict(commits)
50 # calculate hashes of all commits for unique identifier of commits
50 # calculate hashes of all commits for unique identifier of commits
51 # inside that pull request
51 # inside that pull request
52 commits_hash = md5_safe(':'.join(x.get('raw_id', '') for x in commits))
52 commits_hash = md5_safe(':'.join(x.get('raw_id', '') for x in commits))
53
53
54 data.update({
54 data.update({
55 'pullrequest': {
55 'pullrequest': {
56 'title': self.pullrequest.title,
56 'title': self.pullrequest.title,
57 'issues': issues,
57 'issues': issues,
58 'pull_request_id': self.pullrequest.pull_request_id,
58 'pull_request_id': self.pullrequest.pull_request_id,
59 'url': PullRequestModel().get_url(
59 'url': PullRequestModel().get_url(
60 self.pullrequest, request=self.request),
60 self.pullrequest, request=self.request),
61 'permalink_url': PullRequestModel().get_url(
61 'permalink_url': PullRequestModel().get_url(
62 self.pullrequest, request=self.request, permalink=True),
62 self.pullrequest, request=self.request, permalink=True),
63 'shadow_url': PullRequestModel().get_shadow_clone_url(
63 'shadow_url': PullRequestModel().get_shadow_clone_url(
64 self.pullrequest, request=self.request),
64 self.pullrequest, request=self.request),
65 'status': self.pullrequest.calculated_review_status(),
65 'status': self.pullrequest.calculated_review_status(),
66 'commits_uid': commits_hash,
66 'commits_uid': commits_hash,
67 'commits': commits,
67 'commits': commits,
68 }
68 }
69 })
69 })
70 return data
70 return data
71
71
72
72
73 class PullRequestCreateEvent(PullRequestEvent):
73 class PullRequestCreateEvent(PullRequestEvent):
74 """
74 """
75 An instance of this class is emitted as an :term:`event` after a pull
75 An instance of this class is emitted as an :term:`event` after a pull
76 request is created.
76 request is created.
77 """
77 """
78 name = 'pullrequest-create'
78 name = 'pullrequest-create'
79 display_name = lazy_ugettext('pullrequest created')
79 display_name = lazy_ugettext('pullrequest created')
80 description = lazy_ugettext('Event triggered after pull request was created')
80
81
81
82
82 class PullRequestCloseEvent(PullRequestEvent):
83 class PullRequestCloseEvent(PullRequestEvent):
83 """
84 """
84 An instance of this class is emitted as an :term:`event` after a pull
85 An instance of this class is emitted as an :term:`event` after a pull
85 request is closed.
86 request is closed.
86 """
87 """
87 name = 'pullrequest-close'
88 name = 'pullrequest-close'
88 display_name = lazy_ugettext('pullrequest closed')
89 display_name = lazy_ugettext('pullrequest closed')
90 description = lazy_ugettext('Event triggered after pull request was closed')
89
91
90
92
91 class PullRequestUpdateEvent(PullRequestEvent):
93 class PullRequestUpdateEvent(PullRequestEvent):
92 """
94 """
93 An instance of this class is emitted as an :term:`event` after a pull
95 An instance of this class is emitted as an :term:`event` after a pull
94 request's commits have been updated.
96 request's commits have been updated.
95 """
97 """
96 name = 'pullrequest-update'
98 name = 'pullrequest-update'
97 display_name = lazy_ugettext('pullrequest commits updated')
99 display_name = lazy_ugettext('pullrequest commits updated')
100 description = lazy_ugettext('Event triggered after pull requests was updated')
98
101
99
102
100 class PullRequestReviewEvent(PullRequestEvent):
103 class PullRequestReviewEvent(PullRequestEvent):
101 """
104 """
102 An instance of this class is emitted as an :term:`event` after a pull
105 An instance of this class is emitted as an :term:`event` after a pull
103 request review has changed. A status defines new status of review.
106 request review has changed. A status defines new status of review.
104 """
107 """
105 name = 'pullrequest-review'
108 name = 'pullrequest-review'
106 display_name = lazy_ugettext('pullrequest review changed')
109 display_name = lazy_ugettext('pullrequest review changed')
110 description = lazy_ugettext('Event triggered after a review status of a '
111 'pull requests has changed to other.')
107
112
108 def __init__(self, pullrequest, status):
113 def __init__(self, pullrequest, status):
109 super(PullRequestReviewEvent, self).__init__(pullrequest)
114 super(PullRequestReviewEvent, self).__init__(pullrequest)
110 self.status = status
115 self.status = status
111
116
112
117
113 class PullRequestMergeEvent(PullRequestEvent):
118 class PullRequestMergeEvent(PullRequestEvent):
114 """
119 """
115 An instance of this class is emitted as an :term:`event` after a pull
120 An instance of this class is emitted as an :term:`event` after a pull
116 request is merged.
121 request is merged.
117 """
122 """
118 name = 'pullrequest-merge'
123 name = 'pullrequest-merge'
119 display_name = lazy_ugettext('pullrequest merged')
124 display_name = lazy_ugettext('pullrequest merged')
125 description = lazy_ugettext('Event triggered after a successful merge operation '
126 'was executed on a pull request')
120
127
121
128
122 class PullRequestCommentEvent(PullRequestEvent):
129 class PullRequestCommentEvent(PullRequestEvent):
123 """
130 """
124 An instance of this class is emitted as an :term:`event` after a pull
131 An instance of this class is emitted as an :term:`event` after a pull
125 request comment is created.
132 request comment is created.
126 """
133 """
127 name = 'pullrequest-comment'
134 name = 'pullrequest-comment'
128 display_name = lazy_ugettext('pullrequest commented')
135 display_name = lazy_ugettext('pullrequest commented')
136 description = lazy_ugettext('Event triggered after a comment was made on a code '
137 'in the pull request')
129
138
130 def __init__(self, pullrequest, comment):
139 def __init__(self, pullrequest, comment):
131 super(PullRequestCommentEvent, self).__init__(pullrequest)
140 super(PullRequestCommentEvent, self).__init__(pullrequest)
132 self.comment = comment
141 self.comment = comment
133
142
134 def as_dict(self):
143 def as_dict(self):
135 from rhodecode.model.comment import CommentsModel
144 from rhodecode.model.comment import CommentsModel
136 data = super(PullRequestCommentEvent, self).as_dict()
145 data = super(PullRequestCommentEvent, self).as_dict()
137
146
138 status = None
147 status = None
139 if self.comment.status_change:
148 if self.comment.status_change:
140 status = self.comment.status_change[0].status
149 status = self.comment.status_change[0].status
141
150
142 data.update({
151 data.update({
143 'comment': {
152 'comment': {
144 'status': status,
153 'status': status,
145 'text': self.comment.text,
154 'text': self.comment.text,
146 'type': self.comment.comment_type,
155 'type': self.comment.comment_type,
147 'file': self.comment.f_path,
156 'file': self.comment.f_path,
148 'line': self.comment.line_no,
157 'line': self.comment.line_no,
149 'url': CommentsModel().get_url(
158 'url': CommentsModel().get_url(
150 self.comment, request=self.request),
159 self.comment, request=self.request),
151 'permalink_url': CommentsModel().get_url(
160 'permalink_url': CommentsModel().get_url(
152 self.comment, request=self.request, permalink=True),
161 self.comment, request=self.request, permalink=True),
153 }
162 }
154 })
163 })
155 return data
164 return data
@@ -1,370 +1,400 b''
1 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import collections
19 import collections
20 import logging
20 import logging
21 import datetime
21 import datetime
22
22
23 from rhodecode.translation import lazy_ugettext
23 from rhodecode.translation import lazy_ugettext
24 from rhodecode.model.db import User, Repository, Session
24 from rhodecode.model.db import User, Repository, Session
25 from rhodecode.events.base import RhodeCodeIntegrationEvent
25 from rhodecode.events.base import RhodeCodeIntegrationEvent
26 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 def _commits_as_dict(event, commit_ids, repos):
31 def _commits_as_dict(event, commit_ids, repos):
32 """
32 """
33 Helper function to serialize commit_ids
33 Helper function to serialize commit_ids
34
34
35 :param event: class calling this method
35 :param event: class calling this method
36 :param commit_ids: commits to get
36 :param commit_ids: commits to get
37 :param repos: list of repos to check
37 :param repos: list of repos to check
38 """
38 """
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.lib.helpers import (
40 from rhodecode.lib.helpers import (
41 urlify_commit_message, process_patterns, chop_at_smart)
41 urlify_commit_message, process_patterns, chop_at_smart)
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43
43
44 if not repos:
44 if not repos:
45 raise Exception('no repo defined')
45 raise Exception('no repo defined')
46
46
47 if not isinstance(repos, (tuple, list)):
47 if not isinstance(repos, (tuple, list)):
48 repos = [repos]
48 repos = [repos]
49
49
50 if not commit_ids:
50 if not commit_ids:
51 return []
51 return []
52
52
53 needed_commits = list(commit_ids)
53 needed_commits = list(commit_ids)
54
54
55 commits = []
55 commits = []
56 reviewers = []
56 reviewers = []
57 for repo in repos:
57 for repo in repos:
58 if not needed_commits:
58 if not needed_commits:
59 return commits # return early if we have the commits we need
59 return commits # return early if we have the commits we need
60
60
61 vcs_repo = repo.scm_instance(cache=False)
61 vcs_repo = repo.scm_instance(cache=False)
62
62
63 try:
63 try:
64 # use copy of needed_commits since we modify it while iterating
64 # use copy of needed_commits since we modify it while iterating
65 for commit_id in list(needed_commits):
65 for commit_id in list(needed_commits):
66 if commit_id.startswith('tag=>'):
66 if commit_id.startswith('tag=>'):
67 raw_id = commit_id[5:]
67 raw_id = commit_id[5:]
68 cs_data = {
68 cs_data = {
69 'raw_id': commit_id, 'short_id': commit_id,
69 'raw_id': commit_id, 'short_id': commit_id,
70 'branch': None,
70 'branch': None,
71 'git_ref_change': 'tag_add',
71 'git_ref_change': 'tag_add',
72 'message': 'Added new tag {}'.format(raw_id),
72 'message': 'Added new tag {}'.format(raw_id),
73 'author': event.actor.full_contact,
73 'author': event.actor.full_contact,
74 'date': datetime.datetime.now(),
74 'date': datetime.datetime.now(),
75 'refs': {
75 'refs': {
76 'branches': [],
76 'branches': [],
77 'bookmarks': [],
77 'bookmarks': [],
78 'tags': []
78 'tags': []
79 }
79 }
80 }
80 }
81 commits.append(cs_data)
81 commits.append(cs_data)
82
82
83 elif commit_id.startswith('delete_branch=>'):
83 elif commit_id.startswith('delete_branch=>'):
84 raw_id = commit_id[15:]
84 raw_id = commit_id[15:]
85 cs_data = {
85 cs_data = {
86 'raw_id': commit_id, 'short_id': commit_id,
86 'raw_id': commit_id, 'short_id': commit_id,
87 'branch': None,
87 'branch': None,
88 'git_ref_change': 'branch_delete',
88 'git_ref_change': 'branch_delete',
89 'message': 'Deleted branch {}'.format(raw_id),
89 'message': 'Deleted branch {}'.format(raw_id),
90 'author': event.actor.full_contact,
90 'author': event.actor.full_contact,
91 'date': datetime.datetime.now(),
91 'date': datetime.datetime.now(),
92 'refs': {
92 'refs': {
93 'branches': [],
93 'branches': [],
94 'bookmarks': [],
94 'bookmarks': [],
95 'tags': []
95 'tags': []
96 }
96 }
97 }
97 }
98 commits.append(cs_data)
98 commits.append(cs_data)
99
99
100 else:
100 else:
101 try:
101 try:
102 cs = vcs_repo.get_commit(commit_id)
102 cs = vcs_repo.get_commit(commit_id)
103 except CommitDoesNotExistError:
103 except CommitDoesNotExistError:
104 continue # maybe its in next repo
104 continue # maybe its in next repo
105
105
106 cs_data = cs.__json__()
106 cs_data = cs.__json__()
107 cs_data['refs'] = cs._get_refs()
107 cs_data['refs'] = cs._get_refs()
108
108
109 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
109 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
110 cs_data['reviewers'] = reviewers
110 cs_data['reviewers'] = reviewers
111 cs_data['url'] = RepoModel().get_commit_url(
111 cs_data['url'] = RepoModel().get_commit_url(
112 repo, cs_data['raw_id'], request=event.request)
112 repo, cs_data['raw_id'], request=event.request)
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
114 repo, cs_data['raw_id'], request=event.request,
114 repo, cs_data['raw_id'], request=event.request,
115 permalink=True)
115 permalink=True)
116 urlified_message, issues_data = process_patterns(
116 urlified_message, issues_data = process_patterns(
117 cs_data['message'], repo.repo_name)
117 cs_data['message'], repo.repo_name)
118 cs_data['issues'] = issues_data
118 cs_data['issues'] = issues_data
119 cs_data['message_html'] = urlify_commit_message(
119 cs_data['message_html'] = urlify_commit_message(
120 cs_data['message'], repo.repo_name)
120 cs_data['message'], repo.repo_name)
121 cs_data['message_html_title'] = chop_at_smart(
121 cs_data['message_html_title'] = chop_at_smart(
122 cs_data['message'], '\n', suffix_if_chopped='...')
122 cs_data['message'], '\n', suffix_if_chopped='...')
123 commits.append(cs_data)
123 commits.append(cs_data)
124
124
125 needed_commits.remove(commit_id)
125 needed_commits.remove(commit_id)
126
126
127 except Exception:
127 except Exception:
128 log.exception('Failed to extract commits data')
128 log.exception('Failed to extract commits data')
129 # we don't send any commits when crash happens, only full list
129 # we don't send any commits when crash happens, only full list
130 # matters we short circuit then.
130 # matters we short circuit then.
131 return []
131 return []
132
132
133 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
133 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
134 if missing_commits:
134 if missing_commits:
135 log.error('Inconsistent repository state. '
135 log.error('Inconsistent repository state. '
136 'Missing commits: %s', ', '.join(missing_commits))
136 'Missing commits: %s', ', '.join(missing_commits))
137
137
138 return commits
138 return commits
139
139
140
140
141 def _issues_as_dict(commits):
141 def _issues_as_dict(commits):
142 """ Helper function to serialize issues from commits """
142 """ Helper function to serialize issues from commits """
143 issues = {}
143 issues = {}
144 for commit in commits:
144 for commit in commits:
145 for issue in commit['issues']:
145 for issue in commit['issues']:
146 issues[issue['id']] = issue
146 issues[issue['id']] = issue
147 return issues
147 return issues
148
148
149
149
150 class RepoEvent(RhodeCodeIntegrationEvent):
150 class RepoEvent(RhodeCodeIntegrationEvent):
151 """
151 """
152 Base class for events acting on a repository.
152 Base class for events acting on a repository.
153
153
154 :param repo: a :class:`Repository` instance
154 :param repo: a :class:`Repository` instance
155 """
155 """
156
156
157 def __init__(self, repo):
157 def __init__(self, repo):
158 super(RepoEvent, self).__init__()
158 super(RepoEvent, self).__init__()
159 self.repo = repo
159 self.repo = repo
160
160
161 def as_dict(self):
161 def as_dict(self):
162 from rhodecode.model.repo import RepoModel
162 from rhodecode.model.repo import RepoModel
163 data = super(RepoEvent, self).as_dict()
163 data = super(RepoEvent, self).as_dict()
164
164
165 extra_fields = collections.OrderedDict()
165 extra_fields = collections.OrderedDict()
166 for field in self.repo.extra_fields:
166 for field in self.repo.extra_fields:
167 extra_fields[field.field_key] = field.field_value
167 extra_fields[field.field_key] = field.field_value
168
168
169 data.update({
169 data.update({
170 'repo': {
170 'repo': {
171 'repo_id': self.repo.repo_id,
171 'repo_id': self.repo.repo_id,
172 'repo_name': self.repo.repo_name,
172 'repo_name': self.repo.repo_name,
173 'repo_type': self.repo.repo_type,
173 'repo_type': self.repo.repo_type,
174 'url': RepoModel().get_url(
174 'url': RepoModel().get_url(
175 self.repo, request=self.request),
175 self.repo, request=self.request),
176 'permalink_url': RepoModel().get_url(
176 'permalink_url': RepoModel().get_url(
177 self.repo, request=self.request, permalink=True),
177 self.repo, request=self.request, permalink=True),
178 'extra_fields': extra_fields
178 'extra_fields': extra_fields
179 }
179 }
180 })
180 })
181 return data
181 return data
182
182
183
183
184 class RepoCommitCommentEvent(RepoEvent):
184 class RepoCommitCommentEvent(RepoEvent):
185 """
185 """
186 An instance of this class is emitted as an :term:`event` after a comment is made
186 An instance of this class is emitted as an :term:`event` after a comment is made
187 on repository commit.
187 on repository commit.
188 """
188 """
189
190 name = 'repo-commit-comment'
191 display_name = lazy_ugettext('repository commit comment')
192 description = lazy_ugettext('Event triggered after a comment was made '
193 'on commit inside a repository')
194
189 def __init__(self, repo, commit, comment):
195 def __init__(self, repo, commit, comment):
190 super(RepoCommitCommentEvent, self).__init__(repo)
196 super(RepoCommitCommentEvent, self).__init__(repo)
191 self.commit = commit
197 self.commit = commit
192 self.comment = comment
198 self.comment = comment
193
199
194 name = 'repo-commit-comment'
200 def as_dict(self):
195 display_name = lazy_ugettext('repository commit comment')
201 data = super(RepoCommitCommentEvent, self).as_dict()
202 data['commit'] = {
203 'commit_id': self.commit.raw_id,
204 'commit_message': self.commit.message,
205 'commit_branch': self.commit.branch,
206 }
207
208 data['comment'] = {
209 'comment_id': self.comment.comment_id,
210 'comment_text': self.comment.text,
211 'comment_type': self.comment.comment_type,
212 'comment_f_path': self.comment.f_path,
213 'comment_line_no': self.comment.line_no,
214 }
215 return data
196
216
197
217
198 class RepoPreCreateEvent(RepoEvent):
218 class RepoPreCreateEvent(RepoEvent):
199 """
219 """
200 An instance of this class is emitted as an :term:`event` before a repo is
220 An instance of this class is emitted as an :term:`event` before a repo is
201 created.
221 created.
202 """
222 """
203 name = 'repo-pre-create'
223 name = 'repo-pre-create'
204 display_name = lazy_ugettext('repository pre create')
224 display_name = lazy_ugettext('repository pre create')
225 description = lazy_ugettext('Event triggered before repository is created')
205
226
206
227
207 class RepoCreateEvent(RepoEvent):
228 class RepoCreateEvent(RepoEvent):
208 """
229 """
209 An instance of this class is emitted as an :term:`event` whenever a repo is
230 An instance of this class is emitted as an :term:`event` whenever a repo is
210 created.
231 created.
211 """
232 """
212 name = 'repo-create'
233 name = 'repo-create'
213 display_name = lazy_ugettext('repository created')
234 display_name = lazy_ugettext('repository created')
235 description = lazy_ugettext('Event triggered after repository was created')
214
236
215
237
216 class RepoPreDeleteEvent(RepoEvent):
238 class RepoPreDeleteEvent(RepoEvent):
217 """
239 """
218 An instance of this class is emitted as an :term:`event` whenever a repo is
240 An instance of this class is emitted as an :term:`event` whenever a repo is
219 created.
241 created.
220 """
242 """
221 name = 'repo-pre-delete'
243 name = 'repo-pre-delete'
222 display_name = lazy_ugettext('repository pre delete')
244 display_name = lazy_ugettext('repository pre delete')
245 description = lazy_ugettext('Event triggered before a repository is deleted')
223
246
224
247
225 class RepoDeleteEvent(RepoEvent):
248 class RepoDeleteEvent(RepoEvent):
226 """
249 """
227 An instance of this class is emitted as an :term:`event` whenever a repo is
250 An instance of this class is emitted as an :term:`event` whenever a repo is
228 created.
251 created.
229 """
252 """
230 name = 'repo-delete'
253 name = 'repo-delete'
231 display_name = lazy_ugettext('repository deleted')
254 display_name = lazy_ugettext('repository deleted')
255 description = lazy_ugettext('Event triggered after repository was deleted')
232
256
233
257
234 class RepoVCSEvent(RepoEvent):
258 class RepoVCSEvent(RepoEvent):
235 """
259 """
236 Base class for events triggered by the VCS
260 Base class for events triggered by the VCS
237 """
261 """
238 def __init__(self, repo_name, extras):
262 def __init__(self, repo_name, extras):
239 self.repo = Repository.get_by_repo_name(repo_name)
263 self.repo = Repository.get_by_repo_name(repo_name)
240 if not self.repo:
264 if not self.repo:
241 raise Exception('repo by this name %s does not exist' % repo_name)
265 raise Exception('repo by this name %s does not exist' % repo_name)
242 self.extras = extras
266 self.extras = extras
243 super(RepoVCSEvent, self).__init__(self.repo)
267 super(RepoVCSEvent, self).__init__(self.repo)
244
268
245 @property
269 @property
246 def actor(self):
270 def actor(self):
247 if self.extras.get('username'):
271 if self.extras.get('username'):
248 return User.get_by_username(self.extras['username'])
272 return User.get_by_username(self.extras['username'])
249
273
250 @property
274 @property
251 def actor_ip(self):
275 def actor_ip(self):
252 if self.extras.get('ip'):
276 if self.extras.get('ip'):
253 return self.extras['ip']
277 return self.extras['ip']
254
278
255 @property
279 @property
256 def server_url(self):
280 def server_url(self):
257 if self.extras.get('server_url'):
281 if self.extras.get('server_url'):
258 return self.extras['server_url']
282 return self.extras['server_url']
259
283
260 @property
284 @property
261 def request(self):
285 def request(self):
262 return self.extras.get('request') or self.get_request()
286 return self.extras.get('request') or self.get_request()
263
287
264
288
265 class RepoPrePullEvent(RepoVCSEvent):
289 class RepoPrePullEvent(RepoVCSEvent):
266 """
290 """
267 An instance of this class is emitted as an :term:`event` before commits
291 An instance of this class is emitted as an :term:`event` before commits
268 are pulled from a repo.
292 are pulled from a repo.
269 """
293 """
270 name = 'repo-pre-pull'
294 name = 'repo-pre-pull'
271 display_name = lazy_ugettext('repository pre pull')
295 display_name = lazy_ugettext('repository pre pull')
296 description = lazy_ugettext('Event triggered before repository code is pulled')
272
297
273
298
274 class RepoPullEvent(RepoVCSEvent):
299 class RepoPullEvent(RepoVCSEvent):
275 """
300 """
276 An instance of this class is emitted as an :term:`event` after commits
301 An instance of this class is emitted as an :term:`event` after commits
277 are pulled from a repo.
302 are pulled from a repo.
278 """
303 """
279 name = 'repo-pull'
304 name = 'repo-pull'
280 display_name = lazy_ugettext('repository pull')
305 display_name = lazy_ugettext('repository pull')
306 description = lazy_ugettext('Event triggered after repository code was pulled')
281
307
282
308
283 class RepoPrePushEvent(RepoVCSEvent):
309 class RepoPrePushEvent(RepoVCSEvent):
284 """
310 """
285 An instance of this class is emitted as an :term:`event` before commits
311 An instance of this class is emitted as an :term:`event` before commits
286 are pushed to a repo.
312 are pushed to a repo.
287 """
313 """
288 name = 'repo-pre-push'
314 name = 'repo-pre-push'
289 display_name = lazy_ugettext('repository pre push')
315 display_name = lazy_ugettext('repository pre push')
316 description = lazy_ugettext('Event triggered before the code is '
317 'pushed to a repository')
290
318
291
319
292 class RepoPushEvent(RepoVCSEvent):
320 class RepoPushEvent(RepoVCSEvent):
293 """
321 """
294 An instance of this class is emitted as an :term:`event` after commits
322 An instance of this class is emitted as an :term:`event` after commits
295 are pushed to a repo.
323 are pushed to a repo.
296
324
297 :param extras: (optional) dict of data from proxied VCS actions
325 :param extras: (optional) dict of data from proxied VCS actions
298 """
326 """
299 name = 'repo-push'
327 name = 'repo-push'
300 display_name = lazy_ugettext('repository push')
328 display_name = lazy_ugettext('repository push')
329 description = lazy_ugettext('Event triggered after the code was '
330 'pushed to a repository')
301
331
302 def __init__(self, repo_name, pushed_commit_ids, extras):
332 def __init__(self, repo_name, pushed_commit_ids, extras):
303 super(RepoPushEvent, self).__init__(repo_name, extras)
333 super(RepoPushEvent, self).__init__(repo_name, extras)
304 self.pushed_commit_ids = pushed_commit_ids
334 self.pushed_commit_ids = pushed_commit_ids
305 self.new_refs = extras.new_refs
335 self.new_refs = extras.new_refs
306
336
307 def as_dict(self):
337 def as_dict(self):
308 data = super(RepoPushEvent, self).as_dict()
338 data = super(RepoPushEvent, self).as_dict()
309
339
310 def branch_url(branch_name):
340 def branch_url(branch_name):
311 return '{}/changelog?branch={}'.format(
341 return '{}/changelog?branch={}'.format(
312 data['repo']['url'], branch_name)
342 data['repo']['url'], branch_name)
313
343
314 def tag_url(tag_name):
344 def tag_url(tag_name):
315 return '{}/files/{}/'.format(
345 return '{}/files/{}/'.format(
316 data['repo']['url'], tag_name)
346 data['repo']['url'], tag_name)
317
347
318 commits = _commits_as_dict(
348 commits = _commits_as_dict(
319 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
349 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
320
350
321 last_branch = None
351 last_branch = None
322 for commit in reversed(commits):
352 for commit in reversed(commits):
323 commit['branch'] = commit['branch'] or last_branch
353 commit['branch'] = commit['branch'] or last_branch
324 last_branch = commit['branch']
354 last_branch = commit['branch']
325 issues = _issues_as_dict(commits)
355 issues = _issues_as_dict(commits)
326
356
327 branches = set()
357 branches = set()
328 tags = set()
358 tags = set()
329 for commit in commits:
359 for commit in commits:
330 if commit['refs']['tags']:
360 if commit['refs']['tags']:
331 for tag in commit['refs']['tags']:
361 for tag in commit['refs']['tags']:
332 tags.add(tag)
362 tags.add(tag)
333 if commit['branch']:
363 if commit['branch']:
334 branches.add(commit['branch'])
364 branches.add(commit['branch'])
335
365
336 # maybe we have branches in new_refs ?
366 # maybe we have branches in new_refs ?
337 try:
367 try:
338 branches = branches.union(set(self.new_refs['branches']))
368 branches = branches.union(set(self.new_refs['branches']))
339 except Exception:
369 except Exception:
340 pass
370 pass
341
371
342 branches = [
372 branches = [
343 {
373 {
344 'name': branch,
374 'name': branch,
345 'url': branch_url(branch)
375 'url': branch_url(branch)
346 }
376 }
347 for branch in branches
377 for branch in branches
348 ]
378 ]
349
379
350 # maybe we have branches in new_refs ?
380 # maybe we have branches in new_refs ?
351 try:
381 try:
352 tags = tags.union(set(self.new_refs['tags']))
382 tags = tags.union(set(self.new_refs['tags']))
353 except Exception:
383 except Exception:
354 pass
384 pass
355
385
356 tags = [
386 tags = [
357 {
387 {
358 'name': tag,
388 'name': tag,
359 'url': tag_url(tag)
389 'url': tag_url(tag)
360 }
390 }
361 for tag in tags
391 for tag in tags
362 ]
392 ]
363
393
364 data['push'] = {
394 data['push'] = {
365 'commits': commits,
395 'commits': commits,
366 'issues': issues,
396 'issues': issues,
367 'branches': branches,
397 'branches': branches,
368 'tags': tags,
398 'tags': tags,
369 }
399 }
370 return data
400 return data
@@ -1,80 +1,83 b''
1 # Copyright (C) 2016-2020 RhodeCode GmbH
1 # Copyright (C) 2016-2020 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20
20
21 from rhodecode.translation import lazy_ugettext
21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.base import RhodeCodeIntegrationEvent
22 from rhodecode.events.base import RhodeCodeIntegrationEvent
23
23
24
24
25 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
26
26
27
27
28 class RepoGroupEvent(RhodeCodeIntegrationEvent):
28 class RepoGroupEvent(RhodeCodeIntegrationEvent):
29 """
29 """
30 Base class for events acting on a repository group.
30 Base class for events acting on a repository group.
31
31
32 :param repo: a :class:`RepositoryGroup` instance
32 :param repo: a :class:`RepositoryGroup` instance
33 """
33 """
34
34
35 def __init__(self, repo_group):
35 def __init__(self, repo_group):
36 super(RepoGroupEvent, self).__init__()
36 super(RepoGroupEvent, self).__init__()
37 self.repo_group = repo_group
37 self.repo_group = repo_group
38
38
39 def as_dict(self):
39 def as_dict(self):
40 data = super(RepoGroupEvent, self).as_dict()
40 data = super(RepoGroupEvent, self).as_dict()
41 data.update({
41 data.update({
42 'repo_group': {
42 'repo_group': {
43 'group_id': self.repo_group.group_id,
43 'group_id': self.repo_group.group_id,
44 'group_name': self.repo_group.group_name,
44 'group_name': self.repo_group.group_name,
45 'group_parent_id': self.repo_group.group_parent_id,
45 'group_parent_id': self.repo_group.group_parent_id,
46 'group_description': self.repo_group.group_description,
46 'group_description': self.repo_group.group_description,
47 'user_id': self.repo_group.user_id,
47 'user_id': self.repo_group.user_id,
48 'created_by': self.repo_group.user.username,
48 'created_by': self.repo_group.user.username,
49 'created_on': self.repo_group.created_on,
49 'created_on': self.repo_group.created_on,
50 'enable_locking': self.repo_group.enable_locking,
50 'enable_locking': self.repo_group.enable_locking,
51 }
51 }
52 })
52 })
53 return data
53 return data
54
54
55
55
56 class RepoGroupCreateEvent(RepoGroupEvent):
56 class RepoGroupCreateEvent(RepoGroupEvent):
57 """
57 """
58 An instance of this class is emitted as an :term:`event` whenever a
58 An instance of this class is emitted as an :term:`event` whenever a
59 repository group is created.
59 repository group is created.
60 """
60 """
61 name = 'repo-group-create'
61 name = 'repo-group-create'
62 display_name = lazy_ugettext('repository group created')
62 display_name = lazy_ugettext('repository group created')
63 description = lazy_ugettext('Event triggered after a repository group was created')
63
64
64
65
65 class RepoGroupDeleteEvent(RepoGroupEvent):
66 class RepoGroupDeleteEvent(RepoGroupEvent):
66 """
67 """
67 An instance of this class is emitted as an :term:`event` whenever a
68 An instance of this class is emitted as an :term:`event` whenever a
68 repository group is deleted.
69 repository group is deleted.
69 """
70 """
70 name = 'repo-group-delete'
71 name = 'repo-group-delete'
71 display_name = lazy_ugettext('repository group deleted')
72 display_name = lazy_ugettext('repository group deleted')
73 description = lazy_ugettext('Event triggered after a repository group was deleted')
72
74
73
75
74 class RepoGroupUpdateEvent(RepoGroupEvent):
76 class RepoGroupUpdateEvent(RepoGroupEvent):
75 """
77 """
76 An instance of this class is emitted as an :term:`event` whenever a
78 An instance of this class is emitted as an :term:`event` whenever a
77 repository group is updated.
79 repository group is updated.
78 """
80 """
79 name = 'repo-group-update'
81 name = 'repo-group-update'
80 display_name = lazy_ugettext('repository group update')
82 display_name = lazy_ugettext('repository group update')
83 description = lazy_ugettext('Event triggered after a repository group was updated')
@@ -1,363 +1,428 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import string
22 import string
23 import collections
23 import collections
24 import logging
24 import logging
25 import requests
25 import requests
26 import urllib
26 import urllib
27 from requests.adapters import HTTPAdapter
27 from requests.adapters import HTTPAdapter
28 from requests.packages.urllib3.util.retry import Retry
28 from requests.packages.urllib3.util.retry import Retry
29
29
30 from mako import exceptions
30 from mako import exceptions
31
31
32 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.lib.utils2 import safe_str
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34
34
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class UrlTmpl(string.Template):
39 class UrlTmpl(string.Template):
40
40
41 def safe_substitute(self, **kws):
41 def safe_substitute(self, **kws):
42 # url encode the kw for usage in url
42 # url encode the kw for usage in url
43 kws = {k: urllib.quote(safe_str(v)) for k, v in kws.items()}
43 kws = {k: urllib.quote(safe_str(v)) for k, v in kws.items()}
44 return super(UrlTmpl, self).safe_substitute(**kws)
44 return super(UrlTmpl, self).safe_substitute(**kws)
45
45
46
46
47 class IntegrationTypeBase(object):
47 class IntegrationTypeBase(object):
48 """ Base class for IntegrationType plugins """
48 """ Base class for IntegrationType plugins """
49 is_dummy = False
49 is_dummy = False
50 description = ''
50 description = ''
51
51
52 @classmethod
52 @classmethod
53 def icon(cls):
53 def icon(cls):
54 return '''
54 return '''
55 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
55 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
56 <svg
56 <svg
57 xmlns:dc="http://purl.org/dc/elements/1.1/"
57 xmlns:dc="http://purl.org/dc/elements/1.1/"
58 xmlns:cc="http://creativecommons.org/ns#"
58 xmlns:cc="http://creativecommons.org/ns#"
59 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
59 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
60 xmlns:svg="http://www.w3.org/2000/svg"
60 xmlns:svg="http://www.w3.org/2000/svg"
61 xmlns="http://www.w3.org/2000/svg"
61 xmlns="http://www.w3.org/2000/svg"
62 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
62 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
63 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
63 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
64 viewBox="0 -256 1792 1792"
64 viewBox="0 -256 1792 1792"
65 id="svg3025"
65 id="svg3025"
66 version="1.1"
66 version="1.1"
67 inkscape:version="0.48.3.1 r9886"
67 inkscape:version="0.48.3.1 r9886"
68 width="100%"
68 width="100%"
69 height="100%"
69 height="100%"
70 sodipodi:docname="cog_font_awesome.svg">
70 sodipodi:docname="cog_font_awesome.svg">
71 <metadata
71 <metadata
72 id="metadata3035">
72 id="metadata3035">
73 <rdf:RDF>
73 <rdf:RDF>
74 <cc:Work
74 <cc:Work
75 rdf:about="">
75 rdf:about="">
76 <dc:format>image/svg+xml</dc:format>
76 <dc:format>image/svg+xml</dc:format>
77 <dc:type
77 <dc:type
78 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
78 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
79 </cc:Work>
79 </cc:Work>
80 </rdf:RDF>
80 </rdf:RDF>
81 </metadata>
81 </metadata>
82 <defs
82 <defs
83 id="defs3033" />
83 id="defs3033" />
84 <sodipodi:namedview
84 <sodipodi:namedview
85 pagecolor="#ffffff"
85 pagecolor="#ffffff"
86 bordercolor="#666666"
86 bordercolor="#666666"
87 borderopacity="1"
87 borderopacity="1"
88 objecttolerance="10"
88 objecttolerance="10"
89 gridtolerance="10"
89 gridtolerance="10"
90 guidetolerance="10"
90 guidetolerance="10"
91 inkscape:pageopacity="0"
91 inkscape:pageopacity="0"
92 inkscape:pageshadow="2"
92 inkscape:pageshadow="2"
93 inkscape:window-width="640"
93 inkscape:window-width="640"
94 inkscape:window-height="480"
94 inkscape:window-height="480"
95 id="namedview3031"
95 id="namedview3031"
96 showgrid="false"
96 showgrid="false"
97 inkscape:zoom="0.13169643"
97 inkscape:zoom="0.13169643"
98 inkscape:cx="896"
98 inkscape:cx="896"
99 inkscape:cy="896"
99 inkscape:cy="896"
100 inkscape:window-x="0"
100 inkscape:window-x="0"
101 inkscape:window-y="25"
101 inkscape:window-y="25"
102 inkscape:window-maximized="0"
102 inkscape:window-maximized="0"
103 inkscape:current-layer="svg3025" />
103 inkscape:current-layer="svg3025" />
104 <g
104 <g
105 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
105 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
106 id="g3027">
106 id="g3027">
107 <path
107 <path
108 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
108 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
109 id="path3029"
109 id="path3029"
110 inkscape:connector-curvature="0"
110 inkscape:connector-curvature="0"
111 style="fill:currentColor" />
111 style="fill:currentColor" />
112 </g>
112 </g>
113 </svg>
113 </svg>
114 '''
114 '''
115
115
116 def __init__(self, settings):
116 def __init__(self, settings):
117 """
117 """
118 :param settings: dict of settings to be used for the integration
118 :param settings: dict of settings to be used for the integration
119 """
119 """
120 self.settings = settings
120 self.settings = settings
121
121
122 def settings_schema(self):
122 def settings_schema(self):
123 """
123 """
124 A colander schema of settings for the integration type
124 A colander schema of settings for the integration type
125 """
125 """
126 return colander.Schema()
126 return colander.Schema()
127
127
128 def event_enabled(self, event):
129 """
130 Checks if submitted event is enabled based on the plugin settings
131 :param event:
132 :return: bool
133 """
134 allowed_events = self.settings['events']
135 if event.name not in allowed_events:
136 log.debug('event ignored: %r event %s not in allowed set of events %s',
137 event, event.name, allowed_events)
138 return False
139 return True
140
128
141
129 class EEIntegration(IntegrationTypeBase):
142 class EEIntegration(IntegrationTypeBase):
130 description = 'Integration available in RhodeCode EE edition.'
143 description = 'Integration available in RhodeCode EE edition.'
131 is_dummy = True
144 is_dummy = True
132
145
133 def __init__(self, name, key, settings=None):
146 def __init__(self, name, key, settings=None):
134 self.display_name = name
147 self.display_name = name
135 self.key = key
148 self.key = key
136 super(EEIntegration, self).__init__(settings)
149 super(EEIntegration, self).__init__(settings)
137
150
138
151
139 # Helpers #
152 # Helpers #
140 # updating this required to update the `common_vars` as well.
153 # updating this required to update the `common_vars` as well.
141 WEBHOOK_URL_VARS = [
154 WEBHOOK_URL_VARS = [
142 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
155 # GENERAL
143 ('repo_name', 'Full name of the repository'),
156 ('General', [
144 ('repo_type', 'VCS type of repository'),
157 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
145 ('repo_id', 'Unique id of repository'),
158 ('repo_name', 'Full name of the repository'),
146 ('repo_url', 'Repository url'),
159 ('repo_type', 'VCS type of repository'),
160 ('repo_id', 'Unique id of repository'),
161 ('repo_url', 'Repository url'),
162 ]
163 ),
147 # extra repo fields
164 # extra repo fields
148 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
165 ('Repository', [
149
166 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
167 ]
168 ),
150 # special attrs below that we handle, using multi-call
169 # special attrs below that we handle, using multi-call
151 ('branch', 'Name of each branch submitted, if any.'),
170 ('Commit push - Multicalls', [
152 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
171 ('branch', 'Name of each branch submitted, if any.'),
153 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
172 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
154
173 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
174 ]
175 ),
155 # pr events vars
176 # pr events vars
156 ('pull_request_id', 'Unique ID of the pull request.'),
177 ('Pull request', [
157 ('pull_request_title', 'Title of the pull request.'),
178 ('pull_request_id', 'Unique ID of the pull request.'),
158 ('pull_request_url', 'Pull request url.'),
179 ('pull_request_title', 'Title of the pull request.'),
159 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
180 ('pull_request_url', 'Pull request url.'),
160 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
181 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
161 'Changes after PR update'),
182 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
183 'Changes after PR update'),
184 ]
185 ),
186 # commit comment event vars
187 ('Commit comment', [
188 ('commit_comment_id', 'Unique ID of the comment made on a commit.'),
189 ('commit_comment_text', 'Text of commit comment.'),
190 ('commit_comment_type', 'Type of comment, e.g note/todo.'),
162
191
192 ('commit_comment_f_path', 'Optionally path of file for inline comments.'),
193 ('commit_comment_line_no', 'Line number of the file: eg o10, or n200'),
194
195 ('commit_comment_commit_id', 'Commit id that comment was left at.'),
196 ('commit_comment_commit_branch', 'Commit branch that comment was left at'),
197 ('commit_comment_commit_message', 'Commit message that comment was left at'),
198 ]
199 ),
163 # user who triggers the call
200 # user who triggers the call
164 ('username', 'User who triggered the call.'),
201 ('Caller', [
165 ('user_id', 'User id who triggered the call.'),
202 ('username', 'User who triggered the call.'),
203 ('user_id', 'User id who triggered the call.'),
204 ]
205 ),
166 ]
206 ]
167
207
168 # common vars for url template used for CI plugins. Shared with webhook
208 # common vars for url template used for CI plugins. Shared with webhook
169 CI_URL_VARS = WEBHOOK_URL_VARS
209 CI_URL_VARS = WEBHOOK_URL_VARS
170
210
171
211
172 class CommitParsingDataHandler(object):
212 class CommitParsingDataHandler(object):
173
213
174 def aggregate_branch_data(self, branches, commits):
214 def aggregate_branch_data(self, branches, commits):
175 branch_data = collections.OrderedDict()
215 branch_data = collections.OrderedDict()
176 for obj in branches:
216 for obj in branches:
177 branch_data[obj['name']] = obj
217 branch_data[obj['name']] = obj
178
218
179 branches_commits = collections.OrderedDict()
219 branches_commits = collections.OrderedDict()
180 for commit in commits:
220 for commit in commits:
181 if commit.get('git_ref_change'):
221 if commit.get('git_ref_change'):
182 # special case for GIT that allows creating tags,
222 # special case for GIT that allows creating tags,
183 # deleting branches without associated commit
223 # deleting branches without associated commit
184 continue
224 continue
185 commit_branch = commit['branch']
225 commit_branch = commit['branch']
186
226
187 if commit_branch not in branches_commits:
227 if commit_branch not in branches_commits:
188 _branch = branch_data[commit_branch] \
228 _branch = branch_data[commit_branch] \
189 if commit_branch else commit_branch
229 if commit_branch else commit_branch
190 branch_commits = {'branch': _branch,
230 branch_commits = {'branch': _branch,
191 'branch_head': '',
231 'branch_head': '',
192 'commits': []}
232 'commits': []}
193 branches_commits[commit_branch] = branch_commits
233 branches_commits[commit_branch] = branch_commits
194
234
195 branch_commits = branches_commits[commit_branch]
235 branch_commits = branches_commits[commit_branch]
196 branch_commits['commits'].append(commit)
236 branch_commits['commits'].append(commit)
197 branch_commits['branch_head'] = commit['raw_id']
237 branch_commits['branch_head'] = commit['raw_id']
198 return branches_commits
238 return branches_commits
199
239
200
240
201 class WebhookDataHandler(CommitParsingDataHandler):
241 class WebhookDataHandler(CommitParsingDataHandler):
202 name = 'webhook'
242 name = 'webhook'
203
243
204 def __init__(self, template_url, headers):
244 def __init__(self, template_url, headers):
205 self.template_url = template_url
245 self.template_url = template_url
206 self.headers = headers
246 self.headers = headers
207
247
208 def get_base_parsed_template(self, data):
248 def get_base_parsed_template(self, data):
209 """
249 """
210 initially parses the passed in template with some common variables
250 initially parses the passed in template with some common variables
211 available on ALL calls
251 available on ALL calls
212 """
252 """
213 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
253 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
214 common_vars = {
254 common_vars = {
215 'repo_name': data['repo']['repo_name'],
255 'repo_name': data['repo']['repo_name'],
216 'repo_type': data['repo']['repo_type'],
256 'repo_type': data['repo']['repo_type'],
217 'repo_id': data['repo']['repo_id'],
257 'repo_id': data['repo']['repo_id'],
218 'repo_url': data['repo']['url'],
258 'repo_url': data['repo']['url'],
219 'username': data['actor']['username'],
259 'username': data['actor']['username'],
220 'user_id': data['actor']['user_id'],
260 'user_id': data['actor']['user_id'],
221 'event_name': data['name']
261 'event_name': data['name']
222 }
262 }
223
263
224 extra_vars = {}
264 extra_vars = {}
225 for extra_key, extra_val in data['repo']['extra_fields'].items():
265 for extra_key, extra_val in data['repo']['extra_fields'].items():
226 extra_vars['extra__{}'.format(extra_key)] = extra_val
266 extra_vars['extra__{}'.format(extra_key)] = extra_val
227 common_vars.update(extra_vars)
267 common_vars.update(extra_vars)
228
268
229 template_url = self.template_url.replace('${extra:', '${extra__')
269 template_url = self.template_url.replace('${extra:', '${extra__')
230 for k, v in common_vars.items():
270 for k, v in common_vars.items():
231 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
271 template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
232 return template_url
272 return template_url
233
273
234 def repo_push_event_handler(self, event, data):
274 def repo_push_event_handler(self, event, data):
235 url = self.get_base_parsed_template(data)
275 url = self.get_base_parsed_template(data)
236 url_calls = []
276 url_calls = []
237
277
238 branches_commits = self.aggregate_branch_data(
278 branches_commits = self.aggregate_branch_data(
239 data['push']['branches'], data['push']['commits'])
279 data['push']['branches'], data['push']['commits'])
240 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
280 if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
241 # call it multiple times, for each branch if used in variables
281 # call it multiple times, for each branch if used in variables
242 for branch, commit_ids in branches_commits.items():
282 for branch, commit_ids in branches_commits.items():
243 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
283 branch_url = UrlTmpl(url).safe_substitute(branch=branch)
244
284
245 if '${branch_head}' in branch_url:
285 if '${branch_head}' in branch_url:
246 # last commit in the aggregate is the head of the branch
286 # last commit in the aggregate is the head of the branch
247 branch_head = commit_ids['branch_head']
287 branch_head = commit_ids['branch_head']
248 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
288 branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
249
289
250 # call further down for each commit if used
290 # call further down for each commit if used
251 if '${commit_id}' in branch_url:
291 if '${commit_id}' in branch_url:
252 for commit_data in commit_ids['commits']:
292 for commit_data in commit_ids['commits']:
253 commit_id = commit_data['raw_id']
293 commit_id = commit_data['raw_id']
254 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
294 commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
255 # register per-commit call
295 # register per-commit call
256 log.debug(
296 log.debug(
257 'register %s call(%s) to url %s',
297 'register %s call(%s) to url %s',
258 self.name, event, commit_url)
298 self.name, event, commit_url)
259 url_calls.append(
299 url_calls.append(
260 (commit_url, self.headers, data))
300 (commit_url, self.headers, data))
261
301
262 else:
302 else:
263 # register per-branch call
303 # register per-branch call
264 log.debug('register %s call(%s) to url %s',
304 log.debug('register %s call(%s) to url %s',
265 self.name, event, branch_url)
305 self.name, event, branch_url)
266 url_calls.append((branch_url, self.headers, data))
306 url_calls.append((branch_url, self.headers, data))
267
307
268 else:
308 else:
269 log.debug('register %s call(%s) to url %s', self.name, event, url)
309 log.debug('register %s call(%s) to url %s', self.name, event, url)
270 url_calls.append((url, self.headers, data))
310 url_calls.append((url, self.headers, data))
271
311
272 return url_calls
312 return url_calls
273
313
314 def repo_commit_comment_handler(self, event, data):
315 url = self.get_base_parsed_template(data)
316 log.debug('register %s call(%s) to url %s', self.name, event, url)
317 comment_vars = [
318 ('commit_comment_id', data['comment']['comment_id']),
319 ('commit_comment_text', data['comment']['comment_text']),
320 ('commit_comment_type', data['comment']['comment_type']),
321
322 ('commit_comment_f_path', data['comment']['comment_f_path']),
323 ('commit_comment_line_no', data['comment']['comment_line_no']),
324
325 ('commit_comment_commit_id', data['commit']['commit_id']),
326 ('commit_comment_commit_branch', data['commit']['commit_branch']),
327 ('commit_comment_commit_message', data['commit']['commit_message']),
328 ]
329 for k, v in comment_vars:
330 url = UrlTmpl(url).safe_substitute(**{k: v})
331
332 return [(url, self.headers, data)]
333
274 def repo_create_event_handler(self, event, data):
334 def repo_create_event_handler(self, event, data):
275 url = self.get_base_parsed_template(data)
335 url = self.get_base_parsed_template(data)
276 log.debug('register %s call(%s) to url %s', self.name, event, url)
336 log.debug('register %s call(%s) to url %s', self.name, event, url)
277 return [(url, self.headers, data)]
337 return [(url, self.headers, data)]
278
338
279 def pull_request_event_handler(self, event, data):
339 def pull_request_event_handler(self, event, data):
280 url = self.get_base_parsed_template(data)
340 url = self.get_base_parsed_template(data)
281 log.debug('register %s call(%s) to url %s', self.name, event, url)
341 log.debug('register %s call(%s) to url %s', self.name, event, url)
282 pr_vars = [
342 pr_vars = [
283 ('pull_request_id', data['pullrequest']['pull_request_id']),
343 ('pull_request_id', data['pullrequest']['pull_request_id']),
284 ('pull_request_title', data['pullrequest']['title']),
344 ('pull_request_title', data['pullrequest']['title']),
285 ('pull_request_url', data['pullrequest']['url']),
345 ('pull_request_url', data['pullrequest']['url']),
286 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
346 ('pull_request_shadow_url', data['pullrequest']['shadow_url']),
287 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
347 ('pull_request_commits_uid', data['pullrequest']['commits_uid']),
288 ]
348 ]
289 for k, v in pr_vars:
349 for k, v in pr_vars:
290 url = UrlTmpl(url).safe_substitute(**{k: v})
350 url = UrlTmpl(url).safe_substitute(**{k: v})
291
351
292 return [(url, self.headers, data)]
352 return [(url, self.headers, data)]
293
353
294 def __call__(self, event, data):
354 def __call__(self, event, data):
295 from rhodecode import events
355 from rhodecode import events
296
356
297 if isinstance(event, events.RepoPushEvent):
357 if isinstance(event, events.RepoPushEvent):
298 return self.repo_push_event_handler(event, data)
358 return self.repo_push_event_handler(event, data)
299 elif isinstance(event, events.RepoCreateEvent):
359 elif isinstance(event, events.RepoCreateEvent):
300 return self.repo_create_event_handler(event, data)
360 return self.repo_create_event_handler(event, data)
361 elif isinstance(event, events.RepoCommitCommentEvent):
362 return self.repo_commit_comment_handler(event, data)
301 elif isinstance(event, events.PullRequestEvent):
363 elif isinstance(event, events.PullRequestEvent):
302 return self.pull_request_event_handler(event, data)
364 return self.pull_request_event_handler(event, data)
303 else:
365 else:
304 raise ValueError(
366 raise ValueError(
305 'event type `%s` not in supported list: %s' % (
367 'event type `{}` has no handler defined'.format(event.__class__))
306 event.__class__, events))
307
368
308
369
309 def get_auth(settings):
370 def get_auth(settings):
310 from requests.auth import HTTPBasicAuth
371 from requests.auth import HTTPBasicAuth
311 username = settings.get('username')
372 username = settings.get('username')
312 password = settings.get('password')
373 password = settings.get('password')
313 if username and password:
374 if username and password:
314 return HTTPBasicAuth(username, password)
375 return HTTPBasicAuth(username, password)
315 return None
376 return None
316
377
317
378
318 def get_web_token(settings):
379 def get_web_token(settings):
319 return settings['secret_token']
380 return settings['secret_token']
320
381
321
382
322 def get_url_vars(url_vars):
383 def get_url_vars(url_vars):
323 return '\n'.join(
384 items = []
324 '{} - {}'.format('${' + key + '}', explanation)
385
325 for key, explanation in url_vars)
386 for section, section_items in url_vars:
387 items.append('\n*{}*'.format(section))
388 for key, explanation in section_items:
389 items.append(' {} - {}'.format('${' + key + '}', explanation))
390 return '\n'.join(items)
326
391
327
392
328 def render_with_traceback(template, *args, **kwargs):
393 def render_with_traceback(template, *args, **kwargs):
329 try:
394 try:
330 return template.render(*args, **kwargs)
395 return template.render(*args, **kwargs)
331 except Exception:
396 except Exception:
332 log.error(exceptions.text_error_template().render())
397 log.error(exceptions.text_error_template().render())
333 raise
398 raise
334
399
335
400
336 STATUS_400 = (400, 401, 403)
401 STATUS_400 = (400, 401, 403)
337 STATUS_500 = (500, 502, 504)
402 STATUS_500 = (500, 502, 504)
338
403
339
404
340 def requests_retry_call(
405 def requests_retry_call(
341 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
406 retries=3, backoff_factor=0.3, status_forcelist=STATUS_400+STATUS_500,
342 session=None):
407 session=None):
343 """
408 """
344 session = requests_retry_session()
409 session = requests_retry_session()
345 response = session.get('http://example.com')
410 response = session.get('http://example.com')
346
411
347 :param retries:
412 :param retries:
348 :param backoff_factor:
413 :param backoff_factor:
349 :param status_forcelist:
414 :param status_forcelist:
350 :param session:
415 :param session:
351 """
416 """
352 session = session or requests.Session()
417 session = session or requests.Session()
353 retry = Retry(
418 retry = Retry(
354 total=retries,
419 total=retries,
355 read=retries,
420 read=retries,
356 connect=retries,
421 connect=retries,
357 backoff_factor=backoff_factor,
422 backoff_factor=backoff_factor,
358 status_forcelist=status_forcelist,
423 status_forcelist=status_forcelist,
359 )
424 )
360 adapter = HTTPAdapter(max_retries=retry)
425 adapter = HTTPAdapter(max_retries=retry)
361 session.mount('http://', adapter)
426 session.mount('http://', adapter)
362 session.mount('https://', adapter)
427 session.mount('https://', adapter)
363 return session
428 return session
@@ -1,298 +1,330 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
23 import logging
22 import logging
23
24 import colander
24 import colander
25
25 import deform.widget
26 from mako.template import Template
26 from mako.template import Template
27
27
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
29 from rhodecode.translation import _
30 from rhodecode.translation import _
30 from rhodecode.lib.celerylib import run_task
31 from rhodecode.lib.celerylib import run_task
31 from rhodecode.lib.celerylib import tasks
32 from rhodecode.lib.celerylib import tasks
32 from rhodecode.integrations.types.base import (
33 from rhodecode.integrations.types.base import (
33 IntegrationTypeBase, render_with_traceback)
34 IntegrationTypeBase, render_with_traceback)
34
35
35
36
36 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
37
38
38 REPO_PUSH_TEMPLATE_PLAINTEXT = Template('''
39 REPO_PUSH_TEMPLATE_PLAINTEXT = Template('''
39 Commits:
40 Commits:
40
41
41 % for commit in data['push']['commits']:
42 % for commit in data['push']['commits']:
42 ${commit['url']} by ${commit['author']} at ${commit['date']}
43 ${commit['url']} by ${commit['author']} at ${commit['date']}
43 ${commit['message']}
44 ${commit['message']}
44 ----
45 ----
45
46
46 % endfor
47 % endfor
47 ''')
48 ''')
48
49
49 REPO_PUSH_TEMPLATE_HTML = Template('''
50 REPO_PUSH_TEMPLATE_HTML = Template('''
50 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
51 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
51 <html xmlns="http://www.w3.org/1999/xhtml">
52 <html xmlns="http://www.w3.org/1999/xhtml">
52 <head>
53 <head>
53 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
54 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
54 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
55 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
55 <title>${subject}</title>
56 <title>${subject}</title>
56 <style type="text/css">
57 <style type="text/css">
57 /* Based on The MailChimp Reset INLINE: Yes. */
58 /* Based on The MailChimp Reset INLINE: Yes. */
58 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
59 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
59 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
60 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
60 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
61 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
61 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
62 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
62 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
63 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
63 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
64 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
64 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
65 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
65 /* End reset */
66 /* End reset */
66
67
67 /* defaults for images*/
68 /* defaults for images*/
68 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
69 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
69 a img {border:none;}
70 a img {border:none;}
70 .image_fix {display:block;}
71 .image_fix {display:block;}
71
72
72 body {line-height:1.2em;}
73 body {line-height:1.2em;}
73 p {margin: 0 0 20px;}
74 p {margin: 0 0 20px;}
74 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
75 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
75 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
76 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
76 a:focus {outline:none;}
77 a:focus {outline:none;}
77 a:hover {color: #305b91;}
78 a:hover {color: #305b91;}
78 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
79 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
79 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
80 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
80 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
81 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
81 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
82 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
82 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
83 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
83 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
84 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
84 input:focus {outline: 1px solid #979797}
85 input:focus {outline: 1px solid #979797}
85 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
86 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
86 /* Put your iPhone 4g styles in here */
87 /* Put your iPhone 4g styles in here */
87 }
88 }
88
89
89 /* Android targeting */
90 /* Android targeting */
90 @media only screen and (-webkit-device-pixel-ratio:.75){
91 @media only screen and (-webkit-device-pixel-ratio:.75){
91 /* Put CSS for low density (ldpi) Android layouts in here */
92 /* Put CSS for low density (ldpi) Android layouts in here */
92 }
93 }
93 @media only screen and (-webkit-device-pixel-ratio:1){
94 @media only screen and (-webkit-device-pixel-ratio:1){
94 /* Put CSS for medium density (mdpi) Android layouts in here */
95 /* Put CSS for medium density (mdpi) Android layouts in here */
95 }
96 }
96 @media only screen and (-webkit-device-pixel-ratio:1.5){
97 @media only screen and (-webkit-device-pixel-ratio:1.5){
97 /* Put CSS for high density (hdpi) Android layouts in here */
98 /* Put CSS for high density (hdpi) Android layouts in here */
98 }
99 }
99 /* end Android targeting */
100 /* end Android targeting */
100
101
101 </style>
102 </style>
102
103
103 <!-- Targeting Windows Mobile -->
104 <!-- Targeting Windows Mobile -->
104 <!--[if IEMobile 7]>
105 <!--[if IEMobile 7]>
105 <style type="text/css">
106 <style type="text/css">
106
107
107 </style>
108 </style>
108 <![endif]-->
109 <![endif]-->
109
110
110 <!--[if gte mso 9]>
111 <!--[if gte mso 9]>
111 <style>
112 <style>
112 /* Target Outlook 2007 and 2010 */
113 /* Target Outlook 2007 and 2010 */
113 </style>
114 </style>
114 <![endif]-->
115 <![endif]-->
115 </head>
116 </head>
116 <body>
117 <body>
117 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
118 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
118 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
119 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
119 <tr>
120 <tr>
120 <td valign="top" style="padding:0;">
121 <td valign="top" style="padding:0;">
121 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
122 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
122 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
123 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
123 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
124 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
124 ${'RhodeCode'}
125 ${'RhodeCode'}
125 </a>
126 </a>
126 </td></tr>
127 </td></tr>
127 <tr>
128 <tr>
128 <td style="padding:15px;" valign="top">
129 <td style="padding:15px;" valign="top">
129 % if data['push']['commits']:
130 % if data['push']['commits']:
130 % for commit in data['push']['commits']:
131 % for commit in data['push']['commits']:
131 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
132 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
132 ${commit['message_html']} <br/>
133 ${commit['message_html']} <br/>
133 <br/>
134 <br/>
134 % endfor
135 % endfor
135 % else:
136 % else:
136 No commit data
137 No commit data
137 % endif
138 % endif
138 </td>
139 </td>
139 </tr>
140 </tr>
140 </table>
141 </table>
141 </td>
142 </td>
142 </tr>
143 </tr>
143 </table>
144 </table>
144 <!-- End of wrapper table -->
145 <!-- End of wrapper table -->
145 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
146 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
146 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
147 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
147 </a></p>
148 </a></p>
148 </body>
149 </body>
149 </html>
150 </html>
150 ''')
151 ''')
151
152
152
153
153 class EmailSettingsSchema(colander.Schema):
154 class EmailSettingsSchema(colander.Schema):
154 @colander.instantiate(validator=colander.Length(min=1))
155 @colander.instantiate(validator=colander.Length(min=1))
155 class recipients(colander.SequenceSchema):
156 class recipients(colander.SequenceSchema):
156 title = _('Recipients')
157 title = _('Recipients')
157 description = _('Email addresses to send push events to')
158 description = _('Email addresses to send push events to')
158 widget = deform.widget.SequenceWidget(min_len=1)
159 widget = deform.widget.SequenceWidget(min_len=1)
159
160
160 recipient = colander.SchemaNode(
161 recipient = colander.SchemaNode(
161 colander.String(),
162 colander.String(),
162 title=_('Email address'),
163 title=_('Email address'),
163 description=_('Email address'),
164 description=_('Email address'),
164 default='',
165 default='',
165 validator=colander.Email(),
166 validator=colander.Email(),
166 widget=deform.widget.TextInputWidget(
167 widget=deform.widget.TextInputWidget(
167 placeholder='user@domain.com',
168 placeholder='user@domain.com',
168 ),
169 ),
169 )
170 )
170
171
171
172
172 class EmailIntegrationType(IntegrationTypeBase):
173 class EmailIntegrationType(IntegrationTypeBase):
173 key = 'email'
174 key = 'email'
174 display_name = _('Email')
175 display_name = _('Email')
175 description = _('Send repo push summaries to a list of recipients via email')
176 description = _('Send repo push summaries to a list of recipients via email')
176
177
178 valid_events = [
179 events.RepoPushEvent
180 ]
181
177 @classmethod
182 @classmethod
178 def icon(cls):
183 def icon(cls):
179 return '''
184 return '''
180 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
185 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
181 <svg
186 <svg
182 xmlns:dc="http://purl.org/dc/elements/1.1/"
187 xmlns:dc="http://purl.org/dc/elements/1.1/"
183 xmlns:cc="http://creativecommons.org/ns#"
188 xmlns:cc="http://creativecommons.org/ns#"
184 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
189 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
185 xmlns:svg="http://www.w3.org/2000/svg"
190 xmlns:svg="http://www.w3.org/2000/svg"
186 xmlns="http://www.w3.org/2000/svg"
191 xmlns="http://www.w3.org/2000/svg"
187 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
192 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
188 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
193 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
189 viewBox="0 -256 1850 1850"
194 viewBox="0 -256 1850 1850"
190 id="svg2989"
195 id="svg2989"
191 version="1.1"
196 version="1.1"
192 inkscape:version="0.48.3.1 r9886"
197 inkscape:version="0.48.3.1 r9886"
193 width="100%"
198 width="100%"
194 height="100%"
199 height="100%"
195 sodipodi:docname="envelope_font_awesome.svg">
200 sodipodi:docname="envelope_font_awesome.svg">
196 <metadata
201 <metadata
197 id="metadata2999">
202 id="metadata2999">
198 <rdf:RDF>
203 <rdf:RDF>
199 <cc:Work
204 <cc:Work
200 rdf:about="">
205 rdf:about="">
201 <dc:format>image/svg+xml</dc:format>
206 <dc:format>image/svg+xml</dc:format>
202 <dc:type
207 <dc:type
203 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
208 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
204 </cc:Work>
209 </cc:Work>
205 </rdf:RDF>
210 </rdf:RDF>
206 </metadata>
211 </metadata>
207 <defs
212 <defs
208 id="defs2997" />
213 id="defs2997" />
209 <sodipodi:namedview
214 <sodipodi:namedview
210 pagecolor="#ffffff"
215 pagecolor="#ffffff"
211 bordercolor="#666666"
216 bordercolor="#666666"
212 borderopacity="1"
217 borderopacity="1"
213 objecttolerance="10"
218 objecttolerance="10"
214 gridtolerance="10"
219 gridtolerance="10"
215 guidetolerance="10"
220 guidetolerance="10"
216 inkscape:pageopacity="0"
221 inkscape:pageopacity="0"
217 inkscape:pageshadow="2"
222 inkscape:pageshadow="2"
218 inkscape:window-width="640"
223 inkscape:window-width="640"
219 inkscape:window-height="480"
224 inkscape:window-height="480"
220 id="namedview2995"
225 id="namedview2995"
221 showgrid="false"
226 showgrid="false"
222 inkscape:zoom="0.13169643"
227 inkscape:zoom="0.13169643"
223 inkscape:cx="896"
228 inkscape:cx="896"
224 inkscape:cy="896"
229 inkscape:cy="896"
225 inkscape:window-x="0"
230 inkscape:window-x="0"
226 inkscape:window-y="25"
231 inkscape:window-y="25"
227 inkscape:window-maximized="0"
232 inkscape:window-maximized="0"
228 inkscape:current-layer="svg2989" />
233 inkscape:current-layer="svg2989" />
229 <g
234 <g
230 transform="matrix(1,0,0,-1,37.966102,1282.678)"
235 transform="matrix(1,0,0,-1,37.966102,1282.678)"
231 id="g2991">
236 id="g2991">
232 <path
237 <path
233 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
238 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
234 id="path2993"
239 id="path2993"
235 inkscape:connector-curvature="0"
240 inkscape:connector-curvature="0"
236 style="fill:currentColor" />
241 style="fill:currentColor" />
237 </g>
242 </g>
238 </svg>
243 </svg>
239 '''
244 '''
240
245
241 def settings_schema(self):
246 def settings_schema(self):
242 schema = EmailSettingsSchema()
247 schema = EmailSettingsSchema()
248 schema.add(colander.SchemaNode(
249 colander.Set(),
250 widget=CheckboxChoiceWidgetDesc(
251 values=sorted(
252 [(e.name, e.display_name, e.description) for e in self.valid_events]
253 ),
254 ),
255 description="List of events activated for this integration",
256 name='events'
257 ))
243 return schema
258 return schema
244
259
245 def send_event(self, event):
260 def send_event(self, event):
246 data = event.as_dict()
261 log.debug('handling event %s with integration %s', event.name, self)
247 log.debug('got event: %r', event)
262
263 if event.__class__ not in self.valid_events:
264 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
265 return
266
267 if not self.event_enabled(event):
268 # NOTE(marcink): for legacy reasons we're skipping this check...
269 # since the email event haven't had any settings...
270 pass
248
271
272 handler = EmailEventHandler(self.settings)
273 handler(event, event_data=event.as_dict())
274
275
276 class EmailEventHandler(object):
277 def __init__(self, integration_settings):
278 self.integration_settings = integration_settings
279
280 def __call__(self, event, event_data):
249 if isinstance(event, events.RepoPushEvent):
281 if isinstance(event, events.RepoPushEvent):
250 repo_push_handler(data, self.settings)
282 self.repo_push_handler(event, event_data)
251 else:
283 else:
252 log.debug('ignoring event: %r', event)
284 log.debug('ignoring event: %r', event)
253
285
254
286 def repo_push_handler(self, event, data):
255 def repo_push_handler(data, settings):
287 commit_num = len(data['push']['commits'])
256 commit_num = len(data['push']['commits'])
288 server_url = data['server_url']
257 server_url = data['server_url']
258
289
259 if commit_num == 1:
290 if commit_num == 1:
260 if data['push']['branches']:
291 if data['push']['branches']:
261 _subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'
292 _subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'
262 else:
293 else:
263 _subject = '[{repo_name}] {author} pushed {commit_num} commit'
294 _subject = '[{repo_name}] {author} pushed {commit_num} commit'
264 subject = _subject.format(
295 subject = _subject.format(
265 author=data['actor']['username'],
296 author=data['actor']['username'],
266 repo_name=data['repo']['repo_name'],
297 repo_name=data['repo']['repo_name'],
267 commit_num=commit_num,
298 commit_num=commit_num,
268 branches=', '.join(
299 branches=', '.join(
269 branch['name'] for branch in data['push']['branches'])
300 branch['name'] for branch in data['push']['branches'])
270 )
301 )
271 else:
272 if data['push']['branches']:
273 _subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'
274 else:
302 else:
275 _subject = '[{repo_name}] {author} pushed {commit_num} commits'
303 if data['push']['branches']:
276 subject = _subject.format(
304 _subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'
277 author=data['actor']['username'],
305 else:
278 repo_name=data['repo']['repo_name'],
306 _subject = '[{repo_name}] {author} pushed {commit_num} commits'
279 commit_num=commit_num,
307 subject = _subject.format(
280 branches=', '.join(
308 author=data['actor']['username'],
281 branch['name'] for branch in data['push']['branches']))
309 repo_name=data['repo']['repo_name'],
310 commit_num=commit_num,
311 branches=', '.join(
312 branch['name'] for branch in data['push']['branches']))
282
313
283 email_body_plaintext = render_with_traceback(
314 email_body_plaintext = render_with_traceback(
284 REPO_PUSH_TEMPLATE_PLAINTEXT,
315 REPO_PUSH_TEMPLATE_PLAINTEXT,
285 data=data,
316 data=data,
286 subject=subject,
317 subject=subject,
287 instance_url=server_url)
318 instance_url=server_url)
288
319
289 email_body_html = render_with_traceback(
320 email_body_html = render_with_traceback(
290 REPO_PUSH_TEMPLATE_HTML,
321 REPO_PUSH_TEMPLATE_HTML,
291 data=data,
322 data=data,
292 subject=subject,
323 subject=subject,
293 instance_url=server_url)
324 instance_url=server_url)
294
325
295 for email_address in settings['recipients']:
326 recipients = self.integration_settings['recipients']
296 run_task(
327 for email_address in recipients:
297 tasks.send_email, email_address, subject,
328 run_task(
298 email_body_plaintext, email_body_html)
329 tasks.send_email, email_address, subject,
330 email_body_plaintext, email_body_html)
@@ -1,255 +1,251 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
22 import deform
23 import logging
23 import logging
24 import requests
24 import requests
25 import colander
25 import colander
26 import textwrap
26 import textwrap
27 from mako.template import Template
27 from mako.template import Template
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
29 from rhodecode.translation import _
30 from rhodecode.translation import _
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.integrations.types.base import (
34 from rhodecode.integrations.types.base import (
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
35 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
35 requests_retry_call)
36 requests_retry_call)
36
37
37 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
38
39
39 REPO_PUSH_TEMPLATE = Template('''
40 REPO_PUSH_TEMPLATE = Template('''
40 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
41 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
41 <br>
42 <br>
42 <ul>
43 <ul>
43 %for branch, branch_commits in branches_commits.items():
44 %for branch, branch_commits in branches_commits.items():
44 <li>
45 <li>
45 % if branch:
46 % if branch:
46 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
47 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
47 % else:
48 % else:
48 to trunk
49 to trunk
49 % endif
50 % endif
50 <ul>
51 <ul>
51 % for commit in branch_commits['commits']:
52 % for commit in branch_commits['commits']:
52 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
53 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
53 % endfor
54 % endfor
54 </ul>
55 </ul>
55 </li>
56 </li>
56 %endfor
57 %endfor
57 ''')
58 ''')
58
59
59
60
60 class HipchatSettingsSchema(colander.Schema):
61 class HipchatSettingsSchema(colander.Schema):
61 color_choices = [
62 color_choices = [
62 ('yellow', _('Yellow')),
63 ('yellow', _('Yellow')),
63 ('red', _('Red')),
64 ('red', _('Red')),
64 ('green', _('Green')),
65 ('green', _('Green')),
65 ('purple', _('Purple')),
66 ('purple', _('Purple')),
66 ('gray', _('Gray')),
67 ('gray', _('Gray')),
67 ]
68 ]
68
69
69 server_url = colander.SchemaNode(
70 server_url = colander.SchemaNode(
70 colander.String(),
71 colander.String(),
71 title=_('Hipchat server URL'),
72 title=_('Hipchat server URL'),
72 description=_('Hipchat integration url.'),
73 description=_('Hipchat integration url.'),
73 default='',
74 default='',
74 preparer=strip_whitespace,
75 preparer=strip_whitespace,
75 validator=colander.url,
76 validator=colander.url,
76 widget=deform.widget.TextInputWidget(
77 widget=deform.widget.TextInputWidget(
77 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
78 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
78 ),
79 ),
79 )
80 )
80 notify = colander.SchemaNode(
81 notify = colander.SchemaNode(
81 colander.Bool(),
82 colander.Bool(),
82 title=_('Notify'),
83 title=_('Notify'),
83 description=_('Make a notification to the users in room.'),
84 description=_('Make a notification to the users in room.'),
84 missing=False,
85 missing=False,
85 default=False,
86 default=False,
86 )
87 )
87 color = colander.SchemaNode(
88 color = colander.SchemaNode(
88 colander.String(),
89 colander.String(),
89 title=_('Color'),
90 title=_('Color'),
90 description=_('Background color of message.'),
91 description=_('Background color of message.'),
91 missing='',
92 missing='',
92 validator=colander.OneOf([x[0] for x in color_choices]),
93 validator=colander.OneOf([x[0] for x in color_choices]),
93 widget=deform.widget.Select2Widget(
94 widget=deform.widget.Select2Widget(
94 values=color_choices,
95 values=color_choices,
95 ),
96 ),
96 )
97 )
97
98
98
99
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
100 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
100 key = 'hipchat'
101 key = 'hipchat'
101 display_name = _('Hipchat')
102 display_name = _('Hipchat')
102 description = _('Send events such as repo pushes and pull requests to '
103 description = _('Send events such as repo pushes and pull requests to '
103 'your hipchat channel.')
104 'your hipchat channel.')
104
105
105 @classmethod
106 @classmethod
106 def icon(cls):
107 def icon(cls):
107 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
108 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
108
109
109 valid_events = [
110 valid_events = [
110 events.PullRequestCloseEvent,
111 events.PullRequestCloseEvent,
111 events.PullRequestMergeEvent,
112 events.PullRequestMergeEvent,
112 events.PullRequestUpdateEvent,
113 events.PullRequestUpdateEvent,
113 events.PullRequestCommentEvent,
114 events.PullRequestCommentEvent,
114 events.PullRequestReviewEvent,
115 events.PullRequestReviewEvent,
115 events.PullRequestCreateEvent,
116 events.PullRequestCreateEvent,
116 events.RepoPushEvent,
117 events.RepoPushEvent,
117 events.RepoCreateEvent,
118 events.RepoCreateEvent,
118 ]
119 ]
119
120
120 def send_event(self, event):
121 def send_event(self, event):
121 if event.__class__ not in self.valid_events:
122 if event.__class__ not in self.valid_events:
122 log.debug('event not valid: %r', event)
123 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
123 return
124 return
124
125
125 allowed_events = self.settings['events']
126 if not self.event_enabled(event):
126 if event.name not in allowed_events:
127 log.debug('event ignored: %r event %s not in allowed events %s',
128 event, event.name, allowed_events)
129 return
127 return
130
128
131 data = event.as_dict()
129 data = event.as_dict()
132
130
133 text = '<b>%s<b> caused a <b>%s</b> event' % (
131 text = '<b>%s<b> caused a <b>%s</b> event' % (
134 data['actor']['username'], event.name)
132 data['actor']['username'], event.name)
135
133
136 log.debug('handling hipchat event for %s', event.name)
137
138 if isinstance(event, events.PullRequestCommentEvent):
134 if isinstance(event, events.PullRequestCommentEvent):
139 text = self.format_pull_request_comment_event(event, data)
135 text = self.format_pull_request_comment_event(event, data)
140 elif isinstance(event, events.PullRequestReviewEvent):
136 elif isinstance(event, events.PullRequestReviewEvent):
141 text = self.format_pull_request_review_event(event, data)
137 text = self.format_pull_request_review_event(event, data)
142 elif isinstance(event, events.PullRequestEvent):
138 elif isinstance(event, events.PullRequestEvent):
143 text = self.format_pull_request_event(event, data)
139 text = self.format_pull_request_event(event, data)
144 elif isinstance(event, events.RepoPushEvent):
140 elif isinstance(event, events.RepoPushEvent):
145 text = self.format_repo_push_event(data)
141 text = self.format_repo_push_event(data)
146 elif isinstance(event, events.RepoCreateEvent):
142 elif isinstance(event, events.RepoCreateEvent):
147 text = self.format_repo_create_event(data)
143 text = self.format_repo_create_event(data)
148 else:
144 else:
149 log.error('unhandled event type: %r', event)
145 log.error('unhandled event type: %r', event)
150
146
151 run_task(post_text_to_hipchat, self.settings, text)
147 run_task(post_text_to_hipchat, self.settings, text)
152
148
153 def settings_schema(self):
149 def settings_schema(self):
154 schema = HipchatSettingsSchema()
150 schema = HipchatSettingsSchema()
155 schema.add(colander.SchemaNode(
151 schema.add(colander.SchemaNode(
156 colander.Set(),
152 colander.Set(),
157 widget=deform.widget.CheckboxChoiceWidget(
153 widget=CheckboxChoiceWidgetDesc(
158 values=sorted(
154 values=sorted(
159 [(e.name, e.display_name) for e in self.valid_events]
155 [(e.name, e.display_name, e.description) for e in self.valid_events]
160 )
156 ),
161 ),
157 ),
162 description="Events activated for this integration",
158 description="List of events activated for this integration",
163 name='events'
159 name='events'
164 ))
160 ))
165
161
166 return schema
162 return schema
167
163
168 def format_pull_request_comment_event(self, event, data):
164 def format_pull_request_comment_event(self, event, data):
169 comment_text = data['comment']['text']
165 comment_text = data['comment']['text']
170 if len(comment_text) > 200:
166 if len(comment_text) > 200:
171 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
167 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
172 comment_text=h.html_escape(comment_text[:200]),
168 comment_text=h.html_escape(comment_text[:200]),
173 comment_url=data['comment']['url'],
169 comment_url=data['comment']['url'],
174 )
170 )
175
171
176 comment_status = ''
172 comment_status = ''
177 if data['comment']['status']:
173 if data['comment']['status']:
178 comment_status = '[{}]: '.format(data['comment']['status'])
174 comment_status = '[{}]: '.format(data['comment']['status'])
179
175
180 return (textwrap.dedent(
176 return (textwrap.dedent(
181 '''
177 '''
182 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
178 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
183 >>> {comment_status}{comment_text}
179 >>> {comment_status}{comment_text}
184 ''').format(
180 ''').format(
185 comment_status=comment_status,
181 comment_status=comment_status,
186 user=data['actor']['username'],
182 user=data['actor']['username'],
187 number=data['pullrequest']['pull_request_id'],
183 number=data['pullrequest']['pull_request_id'],
188 pr_url=data['pullrequest']['url'],
184 pr_url=data['pullrequest']['url'],
189 pr_status=data['pullrequest']['status'],
185 pr_status=data['pullrequest']['status'],
190 pr_title=h.html_escape(data['pullrequest']['title']),
186 pr_title=h.html_escape(data['pullrequest']['title']),
191 comment_text=h.html_escape(comment_text)
187 comment_text=h.html_escape(comment_text)
192 )
188 )
193 )
189 )
194
190
195 def format_pull_request_review_event(self, event, data):
191 def format_pull_request_review_event(self, event, data):
196 return (textwrap.dedent(
192 return (textwrap.dedent(
197 '''
193 '''
198 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
194 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
199 ''').format(
195 ''').format(
200 user=data['actor']['username'],
196 user=data['actor']['username'],
201 number=data['pullrequest']['pull_request_id'],
197 number=data['pullrequest']['pull_request_id'],
202 pr_url=data['pullrequest']['url'],
198 pr_url=data['pullrequest']['url'],
203 pr_status=data['pullrequest']['status'],
199 pr_status=data['pullrequest']['status'],
204 pr_title=h.html_escape(data['pullrequest']['title']),
200 pr_title=h.html_escape(data['pullrequest']['title']),
205 )
201 )
206 )
202 )
207
203
208 def format_pull_request_event(self, event, data):
204 def format_pull_request_event(self, event, data):
209 action = {
205 action = {
210 events.PullRequestCloseEvent: 'closed',
206 events.PullRequestCloseEvent: 'closed',
211 events.PullRequestMergeEvent: 'merged',
207 events.PullRequestMergeEvent: 'merged',
212 events.PullRequestUpdateEvent: 'updated',
208 events.PullRequestUpdateEvent: 'updated',
213 events.PullRequestCreateEvent: 'created',
209 events.PullRequestCreateEvent: 'created',
214 }.get(event.__class__, str(event.__class__))
210 }.get(event.__class__, str(event.__class__))
215
211
216 return ('Pull request <a href="{url}">#{number}</a> - {title} '
212 return ('Pull request <a href="{url}">#{number}</a> - {title} '
217 '{action} by <b>{user}</b>').format(
213 '{action} by <b>{user}</b>').format(
218 user=data['actor']['username'],
214 user=data['actor']['username'],
219 number=data['pullrequest']['pull_request_id'],
215 number=data['pullrequest']['pull_request_id'],
220 url=data['pullrequest']['url'],
216 url=data['pullrequest']['url'],
221 title=h.html_escape(data['pullrequest']['title']),
217 title=h.html_escape(data['pullrequest']['title']),
222 action=action
218 action=action
223 )
219 )
224
220
225 def format_repo_push_event(self, data):
221 def format_repo_push_event(self, data):
226 branches_commits = self.aggregate_branch_data(
222 branches_commits = self.aggregate_branch_data(
227 data['push']['branches'], data['push']['commits'])
223 data['push']['branches'], data['push']['commits'])
228
224
229 result = render_with_traceback(
225 result = render_with_traceback(
230 REPO_PUSH_TEMPLATE,
226 REPO_PUSH_TEMPLATE,
231 data=data,
227 data=data,
232 branches_commits=branches_commits,
228 branches_commits=branches_commits,
233 )
229 )
234 return result
230 return result
235
231
236 def format_repo_create_event(self, data):
232 def format_repo_create_event(self, data):
237 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
233 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
238 data['repo']['url'],
234 data['repo']['url'],
239 h.html_escape(data['repo']['repo_name']),
235 h.html_escape(data['repo']['repo_name']),
240 data['repo']['repo_type'],
236 data['repo']['repo_type'],
241 data['actor']['username'],
237 data['actor']['username'],
242 )
238 )
243
239
244
240
245 @async_task(ignore_result=True, base=RequestContextTask)
241 @async_task(ignore_result=True, base=RequestContextTask)
246 def post_text_to_hipchat(settings, text):
242 def post_text_to_hipchat(settings, text):
247 log.debug('sending %s to hipchat %s', text, settings['server_url'])
243 log.debug('sending %s to hipchat %s', text, settings['server_url'])
248 json_message = {
244 json_message = {
249 "message": text,
245 "message": text,
250 "color": settings.get('color', 'yellow'),
246 "color": settings.get('color', 'yellow'),
251 "notify": settings.get('notify', False),
247 "notify": settings.get('notify', False),
252 }
248 }
253 req_session = requests_retry_call()
249 req_session = requests_retry_call()
254 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
250 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
255 resp.raise_for_status() # raise exception on a failed request
251 resp.raise_for_status() # raise exception on a failed request
@@ -1,353 +1,351 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import re
22 import re
23 import time
23 import time
24 import textwrap
24 import textwrap
25 import logging
25 import logging
26
26
27 import deform
27 import deform
28 import requests
28 import requests
29 import colander
29 import colander
30 from mako.template import Template
30 from mako.template import Template
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
33 from rhodecode.translation import _
34 from rhodecode.translation import _
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.integrations.types.base import (
38 from rhodecode.integrations.types.base import (
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
39 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
39 requests_retry_call)
40 requests_retry_call)
40
41
41 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
42
43
43
44
44 def html_to_slack_links(message):
45 def html_to_slack_links(message):
45 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
46 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
46 r'<\1|\2>', message)
47 r'<\1|\2>', message)
47
48
48
49
49 REPO_PUSH_TEMPLATE = Template('''
50 REPO_PUSH_TEMPLATE = Template('''
50 <%
51 <%
51 def branch_text(branch):
52 def branch_text(branch):
52 if branch:
53 if branch:
53 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
54 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
54 else:
55 else:
55 ## case for SVN no branch push...
56 ## case for SVN no branch push...
56 return 'to trunk'
57 return 'to trunk'
57 %> \
58 %> \
58
59
59 % for branch, branch_commits in branches_commits.items():
60 % for branch, branch_commits in branches_commits.items():
60 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
61 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
61 % for commit in branch_commits['commits']:
62 % for commit in branch_commits['commits']:
62 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
63 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
63 % endfor
64 % endfor
64 % endfor
65 % endfor
65 ''')
66 ''')
66
67
67
68
68 class SlackSettingsSchema(colander.Schema):
69 class SlackSettingsSchema(colander.Schema):
69 service = colander.SchemaNode(
70 service = colander.SchemaNode(
70 colander.String(),
71 colander.String(),
71 title=_('Slack service URL'),
72 title=_('Slack service URL'),
72 description=h.literal(_(
73 description=h.literal(_(
73 'This can be setup at the '
74 'This can be setup at the '
74 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
75 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
75 'slack app manager</a>')),
76 'slack app manager</a>')),
76 default='',
77 default='',
77 preparer=strip_whitespace,
78 preparer=strip_whitespace,
78 validator=colander.url,
79 validator=colander.url,
79 widget=deform.widget.TextInputWidget(
80 widget=deform.widget.TextInputWidget(
80 placeholder='https://hooks.slack.com/services/...',
81 placeholder='https://hooks.slack.com/services/...',
81 ),
82 ),
82 )
83 )
83 username = colander.SchemaNode(
84 username = colander.SchemaNode(
84 colander.String(),
85 colander.String(),
85 title=_('Username'),
86 title=_('Username'),
86 description=_('Username to show notifications coming from.'),
87 description=_('Username to show notifications coming from.'),
87 missing='Rhodecode',
88 missing='Rhodecode',
88 preparer=strip_whitespace,
89 preparer=strip_whitespace,
89 widget=deform.widget.TextInputWidget(
90 widget=deform.widget.TextInputWidget(
90 placeholder='Rhodecode'
91 placeholder='Rhodecode'
91 ),
92 ),
92 )
93 )
93 channel = colander.SchemaNode(
94 channel = colander.SchemaNode(
94 colander.String(),
95 colander.String(),
95 title=_('Channel'),
96 title=_('Channel'),
96 description=_('Channel to send notifications to.'),
97 description=_('Channel to send notifications to.'),
97 missing='',
98 missing='',
98 preparer=strip_whitespace,
99 preparer=strip_whitespace,
99 widget=deform.widget.TextInputWidget(
100 widget=deform.widget.TextInputWidget(
100 placeholder='#general'
101 placeholder='#general'
101 ),
102 ),
102 )
103 )
103 icon_emoji = colander.SchemaNode(
104 icon_emoji = colander.SchemaNode(
104 colander.String(),
105 colander.String(),
105 title=_('Emoji'),
106 title=_('Emoji'),
106 description=_('Emoji to use eg. :studio_microphone:'),
107 description=_('Emoji to use eg. :studio_microphone:'),
107 missing='',
108 missing='',
108 preparer=strip_whitespace,
109 preparer=strip_whitespace,
109 widget=deform.widget.TextInputWidget(
110 widget=deform.widget.TextInputWidget(
110 placeholder=':studio_microphone:'
111 placeholder=':studio_microphone:'
111 ),
112 ),
112 )
113 )
113
114
114
115
115 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
116 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
116 key = 'slack'
117 key = 'slack'
117 display_name = _('Slack')
118 display_name = _('Slack')
118 description = _('Send events such as repo pushes and pull requests to '
119 description = _('Send events such as repo pushes and pull requests to '
119 'your slack channel.')
120 'your slack channel.')
120
121
121 @classmethod
122 @classmethod
122 def icon(cls):
123 def icon(cls):
123 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
124 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
124
125
125 valid_events = [
126 valid_events = [
126 events.PullRequestCloseEvent,
127 events.PullRequestCloseEvent,
127 events.PullRequestMergeEvent,
128 events.PullRequestMergeEvent,
128 events.PullRequestUpdateEvent,
129 events.PullRequestUpdateEvent,
129 events.PullRequestCommentEvent,
130 events.PullRequestCommentEvent,
130 events.PullRequestReviewEvent,
131 events.PullRequestReviewEvent,
131 events.PullRequestCreateEvent,
132 events.PullRequestCreateEvent,
132 events.RepoPushEvent,
133 events.RepoPushEvent,
133 events.RepoCreateEvent,
134 events.RepoCreateEvent,
134 ]
135 ]
135
136
136 def send_event(self, event):
137 def send_event(self, event):
138 log.debug('handling event %s with integration %s', event.name, self)
139
137 if event.__class__ not in self.valid_events:
140 if event.__class__ not in self.valid_events:
138 log.debug('event not valid: %r', event)
141 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
139 return
142 return
140
143
141 allowed_events = self.settings['events']
144 if not self.event_enabled(event):
142 if event.name not in allowed_events:
143 log.debug('event ignored: %r event %s not in allowed events %s',
144 event, event.name, allowed_events)
145 return
145 return
146
146
147 data = event.as_dict()
147 data = event.as_dict()
148
148
149 # defaults
149 # defaults
150 title = '*%s* caused a *%s* event' % (
150 title = '*%s* caused a *%s* event' % (
151 data['actor']['username'], event.name)
151 data['actor']['username'], event.name)
152 text = '*%s* caused a *%s* event' % (
152 text = '*%s* caused a *%s* event' % (
153 data['actor']['username'], event.name)
153 data['actor']['username'], event.name)
154 fields = None
154 fields = None
155 overrides = None
155 overrides = None
156
156
157 log.debug('handling slack event for %s', event.name)
158
159 if isinstance(event, events.PullRequestCommentEvent):
157 if isinstance(event, events.PullRequestCommentEvent):
160 (title, text, fields, overrides) \
158 (title, text, fields, overrides) \
161 = self.format_pull_request_comment_event(event, data)
159 = self.format_pull_request_comment_event(event, data)
162 elif isinstance(event, events.PullRequestReviewEvent):
160 elif isinstance(event, events.PullRequestReviewEvent):
163 title, text = self.format_pull_request_review_event(event, data)
161 title, text = self.format_pull_request_review_event(event, data)
164 elif isinstance(event, events.PullRequestEvent):
162 elif isinstance(event, events.PullRequestEvent):
165 title, text = self.format_pull_request_event(event, data)
163 title, text = self.format_pull_request_event(event, data)
166 elif isinstance(event, events.RepoPushEvent):
164 elif isinstance(event, events.RepoPushEvent):
167 title, text = self.format_repo_push_event(data)
165 title, text = self.format_repo_push_event(data)
168 elif isinstance(event, events.RepoCreateEvent):
166 elif isinstance(event, events.RepoCreateEvent):
169 title, text = self.format_repo_create_event(data)
167 title, text = self.format_repo_create_event(data)
170 else:
168 else:
171 log.error('unhandled event type: %r', event)
169 log.error('unhandled event type: %r', event)
172
170
173 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
171 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
174
172
175 def settings_schema(self):
173 def settings_schema(self):
176 schema = SlackSettingsSchema()
174 schema = SlackSettingsSchema()
177 schema.add(colander.SchemaNode(
175 schema.add(colander.SchemaNode(
178 colander.Set(),
176 colander.Set(),
179 widget=deform.widget.CheckboxChoiceWidget(
177 widget=CheckboxChoiceWidgetDesc(
180 values=sorted(
178 values=sorted(
181 [(e.name, e.display_name) for e in self.valid_events]
179 [(e.name, e.display_name, e.description) for e in self.valid_events]
182 )
180 ),
183 ),
181 ),
184 description="Events activated for this integration",
182 description="List of events activated for this integration",
185 name='events'
183 name='events'
186 ))
184 ))
187
185
188 return schema
186 return schema
189
187
190 def format_pull_request_comment_event(self, event, data):
188 def format_pull_request_comment_event(self, event, data):
191 comment_text = data['comment']['text']
189 comment_text = data['comment']['text']
192 if len(comment_text) > 200:
190 if len(comment_text) > 200:
193 comment_text = '<{comment_url}|{comment_text}...>'.format(
191 comment_text = '<{comment_url}|{comment_text}...>'.format(
194 comment_text=comment_text[:200],
192 comment_text=comment_text[:200],
195 comment_url=data['comment']['url'],
193 comment_url=data['comment']['url'],
196 )
194 )
197
195
198 fields = None
196 fields = None
199 overrides = None
197 overrides = None
200 status_text = None
198 status_text = None
201
199
202 if data['comment']['status']:
200 if data['comment']['status']:
203 status_color = {
201 status_color = {
204 'approved': '#0ac878',
202 'approved': '#0ac878',
205 'rejected': '#e85e4d'}.get(data['comment']['status'])
203 'rejected': '#e85e4d'}.get(data['comment']['status'])
206
204
207 if status_color:
205 if status_color:
208 overrides = {"color": status_color}
206 overrides = {"color": status_color}
209
207
210 status_text = data['comment']['status']
208 status_text = data['comment']['status']
211
209
212 if data['comment']['file']:
210 if data['comment']['file']:
213 fields = [
211 fields = [
214 {
212 {
215 "title": "file",
213 "title": "file",
216 "value": data['comment']['file']
214 "value": data['comment']['file']
217 },
215 },
218 {
216 {
219 "title": "line",
217 "title": "line",
220 "value": data['comment']['line']
218 "value": data['comment']['line']
221 }
219 }
222 ]
220 ]
223
221
224 template = Template(textwrap.dedent(r'''
222 template = Template(textwrap.dedent(r'''
225 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
223 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
226 '''))
224 '''))
227 title = render_with_traceback(
225 title = render_with_traceback(
228 template, data=data, comment=event.comment)
226 template, data=data, comment=event.comment)
229
227
230 template = Template(textwrap.dedent(r'''
228 template = Template(textwrap.dedent(r'''
231 *pull request title*: ${pr_title}
229 *pull request title*: ${pr_title}
232 % if status_text:
230 % if status_text:
233 *submitted status*: `${status_text}`
231 *submitted status*: `${status_text}`
234 % endif
232 % endif
235 >>> ${comment_text}
233 >>> ${comment_text}
236 '''))
234 '''))
237 text = render_with_traceback(
235 text = render_with_traceback(
238 template,
236 template,
239 comment_text=comment_text,
237 comment_text=comment_text,
240 pr_title=data['pullrequest']['title'],
238 pr_title=data['pullrequest']['title'],
241 status_text=status_text)
239 status_text=status_text)
242
240
243 return title, text, fields, overrides
241 return title, text, fields, overrides
244
242
245 def format_pull_request_review_event(self, event, data):
243 def format_pull_request_review_event(self, event, data):
246 template = Template(textwrap.dedent(r'''
244 template = Template(textwrap.dedent(r'''
247 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
245 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
248 '''))
246 '''))
249 title = render_with_traceback(template, data=data)
247 title = render_with_traceback(template, data=data)
250
248
251 template = Template(textwrap.dedent(r'''
249 template = Template(textwrap.dedent(r'''
252 *pull request title*: ${pr_title}
250 *pull request title*: ${pr_title}
253 '''))
251 '''))
254 text = render_with_traceback(
252 text = render_with_traceback(
255 template,
253 template,
256 pr_title=data['pullrequest']['title'])
254 pr_title=data['pullrequest']['title'])
257
255
258 return title, text
256 return title, text
259
257
260 def format_pull_request_event(self, event, data):
258 def format_pull_request_event(self, event, data):
261 action = {
259 action = {
262 events.PullRequestCloseEvent: 'closed',
260 events.PullRequestCloseEvent: 'closed',
263 events.PullRequestMergeEvent: 'merged',
261 events.PullRequestMergeEvent: 'merged',
264 events.PullRequestUpdateEvent: 'updated',
262 events.PullRequestUpdateEvent: 'updated',
265 events.PullRequestCreateEvent: 'created',
263 events.PullRequestCreateEvent: 'created',
266 }.get(event.__class__, str(event.__class__))
264 }.get(event.__class__, str(event.__class__))
267
265
268 template = Template(textwrap.dedent(r'''
266 template = Template(textwrap.dedent(r'''
269 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
267 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
270 '''))
268 '''))
271 title = render_with_traceback(template, data=data, action=action)
269 title = render_with_traceback(template, data=data, action=action)
272
270
273 template = Template(textwrap.dedent(r'''
271 template = Template(textwrap.dedent(r'''
274 *pull request title*: ${pr_title}
272 *pull request title*: ${pr_title}
275 %if data['pullrequest']['commits']:
273 %if data['pullrequest']['commits']:
276 *commits*: ${len(data['pullrequest']['commits'])}
274 *commits*: ${len(data['pullrequest']['commits'])}
277 %endif
275 %endif
278 '''))
276 '''))
279 text = render_with_traceback(
277 text = render_with_traceback(
280 template,
278 template,
281 pr_title=data['pullrequest']['title'],
279 pr_title=data['pullrequest']['title'],
282 data=data)
280 data=data)
283
281
284 return title, text
282 return title, text
285
283
286 def format_repo_push_event(self, data):
284 def format_repo_push_event(self, data):
287 branches_commits = self.aggregate_branch_data(
285 branches_commits = self.aggregate_branch_data(
288 data['push']['branches'], data['push']['commits'])
286 data['push']['branches'], data['push']['commits'])
289
287
290 template = Template(r'''
288 template = Template(r'''
291 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
289 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
292 ''')
290 ''')
293 title = render_with_traceback(template, data=data)
291 title = render_with_traceback(template, data=data)
294
292
295 text = render_with_traceback(
293 text = render_with_traceback(
296 REPO_PUSH_TEMPLATE,
294 REPO_PUSH_TEMPLATE,
297 data=data,
295 data=data,
298 branches_commits=branches_commits,
296 branches_commits=branches_commits,
299 html_to_slack_links=html_to_slack_links,
297 html_to_slack_links=html_to_slack_links,
300 )
298 )
301
299
302 return title, text
300 return title, text
303
301
304 def format_repo_create_event(self, data):
302 def format_repo_create_event(self, data):
305 template = Template(r'''
303 template = Template(r'''
306 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
304 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
307 ''')
305 ''')
308 title = render_with_traceback(template, data=data)
306 title = render_with_traceback(template, data=data)
309
307
310 template = Template(textwrap.dedent(r'''
308 template = Template(textwrap.dedent(r'''
311 repo_url: ${data['repo']['url']}
309 repo_url: ${data['repo']['url']}
312 repo_type: ${data['repo']['repo_type']}
310 repo_type: ${data['repo']['repo_type']}
313 '''))
311 '''))
314 text = render_with_traceback(template, data=data)
312 text = render_with_traceback(template, data=data)
315
313
316 return title, text
314 return title, text
317
315
318
316
319 @async_task(ignore_result=True, base=RequestContextTask)
317 @async_task(ignore_result=True, base=RequestContextTask)
320 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
318 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
321 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
319 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
322
320
323 fields = fields or []
321 fields = fields or []
324 overrides = overrides or {}
322 overrides = overrides or {}
325
323
326 message_data = {
324 message_data = {
327 "fallback": text,
325 "fallback": text,
328 "color": "#427cc9",
326 "color": "#427cc9",
329 "pretext": title,
327 "pretext": title,
330 #"author_name": "Bobby Tables",
328 #"author_name": "Bobby Tables",
331 #"author_link": "http://flickr.com/bobby/",
329 #"author_link": "http://flickr.com/bobby/",
332 #"author_icon": "http://flickr.com/icons/bobby.jpg",
330 #"author_icon": "http://flickr.com/icons/bobby.jpg",
333 #"title": "Slack API Documentation",
331 #"title": "Slack API Documentation",
334 #"title_link": "https://api.slack.com/",
332 #"title_link": "https://api.slack.com/",
335 "text": text,
333 "text": text,
336 "fields": fields,
334 "fields": fields,
337 #"image_url": "http://my-website.com/path/to/image.jpg",
335 #"image_url": "http://my-website.com/path/to/image.jpg",
338 #"thumb_url": "http://example.com/path/to/thumb.png",
336 #"thumb_url": "http://example.com/path/to/thumb.png",
339 "footer": "RhodeCode",
337 "footer": "RhodeCode",
340 #"footer_icon": "",
338 #"footer_icon": "",
341 "ts": time.time(),
339 "ts": time.time(),
342 "mrkdwn_in": ["pretext", "text"]
340 "mrkdwn_in": ["pretext", "text"]
343 }
341 }
344 message_data.update(overrides)
342 message_data.update(overrides)
345 json_message = {
343 json_message = {
346 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
344 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
347 "channel": settings.get('channel', ''),
345 "channel": settings.get('channel', ''),
348 "username": settings.get('username', 'Rhodecode'),
346 "username": settings.get('username', 'Rhodecode'),
349 "attachments": [message_data]
347 "attachments": [message_data]
350 }
348 }
351 req_session = requests_retry_call()
349 req_session = requests_retry_call()
352 resp = req_session.post(settings['service'], json=json_message, timeout=60)
350 resp = req_session.post(settings['service'], json=json_message, timeout=60)
353 resp.raise_for_status() # raise exception on a failed request
351 resp.raise_for_status() # raise exception on a failed request
@@ -1,265 +1,264 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22
22
23 import deform
24 import deform.widget
23 import deform.widget
25 import logging
24 import logging
26 import colander
25 import colander
27
26
28 import rhodecode
27 import rhodecode
29 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.lib.colander_utils import strip_whitespace
30 from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
30 from rhodecode.translation import _
31 from rhodecode.translation import _
31 from rhodecode.integrations.types.base import (
32 from rhodecode.integrations.types.base import (
32 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
33 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
33 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
34 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
34 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.model.validation_schema import widgets
36 from rhodecode.model.validation_schema import widgets
36
37
37 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
38
39
39
40
40 # updating this required to update the `common_vars` passed in url calling func
41 # updating this required to update the `common_vars` passed in url calling func
41
42
42 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
43 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
43
44
44
45
45 class WebhookSettingsSchema(colander.Schema):
46 class WebhookSettingsSchema(colander.Schema):
46 url = colander.SchemaNode(
47 url = colander.SchemaNode(
47 colander.String(),
48 colander.String(),
48 title=_('Webhook URL'),
49 title=_('Webhook URL'),
49 description=
50 description=
50 _('URL to which Webhook should submit data. If used some of the '
51 _('URL to which Webhook should submit data. If used some of the '
51 'variables would trigger multiple calls, like ${branch} or '
52 'variables would trigger multiple calls, like ${branch} or '
52 '${commit_id}. Webhook will be called as many times as unique '
53 '${commit_id}. Webhook will be called as many times as unique '
53 'objects in data in such cases.'),
54 'objects in data in such cases.'),
54 missing=colander.required,
55 missing=colander.required,
55 required=True,
56 required=True,
57 preparer=strip_whitespace,
56 validator=colander.url,
58 validator=colander.url,
57 widget=widgets.CodeMirrorWidget(
59 widget=widgets.CodeMirrorWidget(
58 help_block_collapsable_name='Show url variables',
60 help_block_collapsable_name='Show url variables',
59 help_block_collapsable=(
61 help_block_collapsable=(
60 'E.g http://my-serv/trigger_job/${{event_name}}'
62 'E.g http://my-serv.com/trigger_job/${{event_name}}'
61 '?PR_ID=${{pull_request_id}}'
63 '?PR_ID=${{pull_request_id}}'
62 '\nFull list of vars:\n{}'.format(URL_VARS)),
64 '\nFull list of vars:\n{}'.format(URL_VARS)),
63 codemirror_mode='text',
65 codemirror_mode='text',
64 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
66 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
65 )
67 )
66 secret_token = colander.SchemaNode(
68 secret_token = colander.SchemaNode(
67 colander.String(),
69 colander.String(),
68 title=_('Secret Token'),
70 title=_('Secret Token'),
69 description=_('Optional string used to validate received payloads. '
71 description=_('Optional string used to validate received payloads. '
70 'It will be sent together with event data in JSON'),
72 'It will be sent together with event data in JSON'),
71 default='',
73 default='',
72 missing='',
74 missing='',
73 widget=deform.widget.TextInputWidget(
75 widget=deform.widget.TextInputWidget(
74 placeholder='e.g. secret_token'
76 placeholder='e.g. secret_token'
75 ),
77 ),
76 )
78 )
77 username = colander.SchemaNode(
79 username = colander.SchemaNode(
78 colander.String(),
80 colander.String(),
79 title=_('Username'),
81 title=_('Username'),
80 description=_('Optional username to authenticate the call.'),
82 description=_('Optional username to authenticate the call.'),
81 default='',
83 default='',
82 missing='',
84 missing='',
83 widget=deform.widget.TextInputWidget(
85 widget=deform.widget.TextInputWidget(
84 placeholder='e.g. admin'
86 placeholder='e.g. admin'
85 ),
87 ),
86 )
88 )
87 password = colander.SchemaNode(
89 password = colander.SchemaNode(
88 colander.String(),
90 colander.String(),
89 title=_('Password'),
91 title=_('Password'),
90 description=_('Optional password to authenticate the call.'),
92 description=_('Optional password to authenticate the call.'),
91 default='',
93 default='',
92 missing='',
94 missing='',
93 widget=deform.widget.PasswordWidget(
95 widget=deform.widget.PasswordWidget(
94 placeholder='e.g. secret.',
96 placeholder='e.g. secret.',
95 redisplay=True,
97 redisplay=True,
96 ),
98 ),
97 )
99 )
98 custom_header_key = colander.SchemaNode(
100 custom_header_key = colander.SchemaNode(
99 colander.String(),
101 colander.String(),
100 title=_('Custom Header Key'),
102 title=_('Custom Header Key'),
101 description=_('Custom Header name to be set when calling endpoint.'),
103 description=_('Custom Header name to be set when calling endpoint.'),
102 default='',
104 default='',
103 missing='',
105 missing='',
104 widget=deform.widget.TextInputWidget(
106 widget=deform.widget.TextInputWidget(
105 placeholder='e.g: Authorization'
107 placeholder='e.g: Authorization'
106 ),
108 ),
107 )
109 )
108 custom_header_val = colander.SchemaNode(
110 custom_header_val = colander.SchemaNode(
109 colander.String(),
111 colander.String(),
110 title=_('Custom Header Value'),
112 title=_('Custom Header Value'),
111 description=_('Custom Header value to be set when calling endpoint.'),
113 description=_('Custom Header value to be set when calling endpoint.'),
112 default='',
114 default='',
113 missing='',
115 missing='',
114 widget=deform.widget.TextInputWidget(
116 widget=deform.widget.TextInputWidget(
115 placeholder='e.g. Basic XxXxXx'
117 placeholder='e.g. Basic XxXxXx'
116 ),
118 ),
117 )
119 )
118 method_type = colander.SchemaNode(
120 method_type = colander.SchemaNode(
119 colander.String(),
121 colander.String(),
120 title=_('Call Method'),
122 title=_('Call Method'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
123 description=_('Select a HTTP method to use when calling the Webhook.'),
122 default='post',
124 default='post',
123 missing='',
125 missing='',
124 widget=deform.widget.RadioChoiceWidget(
126 widget=deform.widget.RadioChoiceWidget(
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
127 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
126 inline=True
128 inline=True
127 ),
129 ),
128 )
130 )
129
131
130
132
131 class WebhookIntegrationType(IntegrationTypeBase):
133 class WebhookIntegrationType(IntegrationTypeBase):
132 key = 'webhook'
134 key = 'webhook'
133 display_name = _('Webhook')
135 display_name = _('Webhook')
134 description = _('send JSON data to a url endpoint')
136 description = _('send JSON data to a url endpoint')
135
137
136 @classmethod
138 @classmethod
137 def icon(cls):
139 def icon(cls):
138 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
140 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
139
141
140 valid_events = [
142 valid_events = [
141 events.PullRequestCloseEvent,
143 events.PullRequestCloseEvent,
142 events.PullRequestMergeEvent,
144 events.PullRequestMergeEvent,
143 events.PullRequestUpdateEvent,
145 events.PullRequestUpdateEvent,
144 events.PullRequestCommentEvent,
146 events.PullRequestCommentEvent,
145 events.PullRequestReviewEvent,
147 events.PullRequestReviewEvent,
146 events.PullRequestCreateEvent,
148 events.PullRequestCreateEvent,
147 events.RepoPushEvent,
149 events.RepoPushEvent,
148 events.RepoCreateEvent,
150 events.RepoCreateEvent,
151 events.RepoCommitCommentEvent,
149 ]
152 ]
150
153
151 def settings_schema(self):
154 def settings_schema(self):
152 schema = WebhookSettingsSchema()
155 schema = WebhookSettingsSchema()
153 schema.add(colander.SchemaNode(
156 schema.add(colander.SchemaNode(
154 colander.Set(),
157 colander.Set(),
155 widget=deform.widget.CheckboxChoiceWidget(
158 widget=CheckboxChoiceWidgetDesc(
156 values=sorted(
159 values=sorted(
157 [(e.name, e.display_name) for e in self.valid_events]
160 [(e.name, e.display_name, e.description) for e in self.valid_events]
158 )
161 ),
159 ),
162 ),
160 description="Events activated for this integration",
163 description="List of events activated for this integration",
161 name='events'
164 name='events'
162 ))
165 ))
163 return schema
166 return schema
164
167
165 def send_event(self, event):
168 def send_event(self, event):
166 log.debug(
169 log.debug('handling event %s with integration %s', event.name, self)
167 'handling event %s with Webhook integration %s', event.name, self)
168
170
169 if event.__class__ not in self.valid_events:
171 if event.__class__ not in self.valid_events:
170 log.debug('event not valid: %r', event)
172 log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
171 return
173 return
172
174
173 allowed_events = self.settings['events']
175 if not self.event_enabled(event):
174 if event.name not in allowed_events:
175 log.debug('event ignored: %r event %s not in allowed events %s',
176 event, event.name, allowed_events)
177 return
176 return
178
177
179 data = event.as_dict()
178 data = event.as_dict()
180 template_url = self.settings['url']
179 template_url = self.settings['url']
181
180
182 headers = {}
181 headers = {}
183 head_key = self.settings.get('custom_header_key')
182 head_key = self.settings.get('custom_header_key')
184 head_val = self.settings.get('custom_header_val')
183 head_val = self.settings.get('custom_header_val')
185 if head_key and head_val:
184 if head_key and head_val:
186 headers = {head_key: head_val}
185 headers = {head_key: head_val}
187
186
188 handler = WebhookDataHandler(template_url, headers)
187 handler = WebhookDataHandler(template_url, headers)
189
188
190 url_calls = handler(event, data)
189 url_calls = handler(event, data)
191 log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
190 log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
192
191
193 run_task(post_to_webhook, url_calls, self.settings)
192 run_task(post_to_webhook, url_calls, self.settings)
194
193
195
194
196 @async_task(ignore_result=True, base=RequestContextTask)
195 @async_task(ignore_result=True, base=RequestContextTask)
197 def post_to_webhook(url_calls, settings):
196 def post_to_webhook(url_calls, settings):
198 """
197 """
199 Example data::
198 Example data::
200
199
201 {'actor': {'user_id': 2, 'username': u'admin'},
200 {'actor': {'user_id': 2, 'username': u'admin'},
202 'actor_ip': u'192.168.157.1',
201 'actor_ip': u'192.168.157.1',
203 'name': 'repo-push',
202 'name': 'repo-push',
204 'push': {'branches': [{'name': u'default',
203 'push': {'branches': [{'name': u'default',
205 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
204 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
206 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
205 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
207 'branch': u'default',
206 'branch': u'default',
208 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
207 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
209 'issues': [],
208 'issues': [],
210 'mentions': [],
209 'mentions': [],
211 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
210 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
211 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
213 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
214 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
213 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
215 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
214 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
216 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
215 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
217 'refs': {'bookmarks': [],
216 'refs': {'bookmarks': [],
218 'branches': [u'default'],
217 'branches': [u'default'],
219 'tags': [u'tip']},
218 'tags': [u'tip']},
220 'reviewers': [],
219 'reviewers': [],
221 'revision': 9L,
220 'revision': 9L,
222 'short_id': 'a815cc738b96',
221 'short_id': 'a815cc738b96',
223 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
222 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
224 'issues': {}},
223 'issues': {}},
225 'repo': {'extra_fields': '',
224 'repo': {'extra_fields': '',
226 'permalink_url': u'http://rc.local:8080/_7',
225 'permalink_url': u'http://rc.local:8080/_7',
227 'repo_id': 7,
226 'repo_id': 7,
228 'repo_name': u'hg-repo',
227 'repo_name': u'hg-repo',
229 'repo_type': u'hg',
228 'repo_type': u'hg',
230 'url': u'http://rc.local:8080/hg-repo'},
229 'url': u'http://rc.local:8080/hg-repo'},
231 'server_url': u'http://rc.local:8080',
230 'server_url': u'http://rc.local:8080',
232 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
231 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
233 }
232 }
234 """
233 """
235
234
236 call_headers = {
235 call_headers = {
237 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
236 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
238 } # updated below with custom ones, allows override
237 } # updated below with custom ones, allows override
239
238
240 auth = get_auth(settings)
239 auth = get_auth(settings)
241 token = get_web_token(settings)
240 token = get_web_token(settings)
242
241
243 for url, headers, data in url_calls:
242 for url, headers, data in url_calls:
244 req_session = requests_retry_call()
243 req_session = requests_retry_call()
245
244
246 method = settings.get('method_type') or 'post'
245 method = settings.get('method_type') or 'post'
247 call_method = getattr(req_session, method)
246 call_method = getattr(req_session, method)
248
247
249 headers = headers or {}
248 headers = headers or {}
250 call_headers.update(headers)
249 call_headers.update(headers)
251
250
252 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
251 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
253 if settings.get('log_data'):
252 if settings.get('log_data'):
254 log.debug('calling webhook with data: %s', data)
253 log.debug('calling webhook with data: %s', data)
255 resp = call_method(url, json={
254 resp = call_method(url, json={
256 'token': token,
255 'token': token,
257 'event': data
256 'event': data
258 }, headers=call_headers, auth=auth, timeout=60)
257 }, headers=call_headers, auth=auth, timeout=60)
259 log.debug('Got Webhook response: %s', resp)
258 log.debug('Got Webhook response: %s', resp)
260
259
261 try:
260 try:
262 resp.raise_for_status() # raise exception on a failed request
261 resp.raise_for_status() # raise exception on a failed request
263 except Exception:
262 except Exception:
264 log.error(resp.text)
263 log.error(resp.text)
265 raise
264 raise
@@ -1,32 +1,59 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import deform
24 import deform.widget
23 import deform.widget
24 from deform.widget import null, OptGroup, string_types
25
26 log = logging.getLogger(__name__)
25
27
26
28
27 log = logging.getLogger(__name__)
29 def _normalize_choices(values):
30 result = []
31 for item in values:
32 if isinstance(item, OptGroup):
33 normalized_options = _normalize_choices(item.options)
34 result.append(OptGroup(item.label, *normalized_options))
35 else:
36 value, description, help_block = item
37 if not isinstance(value, string_types):
38 value = str(value)
39 result.append((value, description, help_block))
40 return result
28
41
29
42
30 class CodeMirrorWidget(deform.widget.TextAreaWidget):
43 class CodeMirrorWidget(deform.widget.TextAreaWidget):
31 template = 'codemirror'
44 template = 'codemirror'
32 requirements = (('deform', None), ('codemirror', None))
45 requirements = (('deform', None), ('codemirror', None))
46
47
48 class CheckboxChoiceWidgetDesc(deform.widget.CheckboxChoiceWidget):
49 template = "checkbox_choice_desc"
50
51 def serialize(self, field, cstruct, **kw):
52 if cstruct in (null, None):
53 cstruct = ()
54 readonly = kw.get("readonly", self.readonly)
55 values = kw.get("values", self.values)
56 kw["values"] = _normalize_choices(values)
57 template = readonly and self.readonly_template or self.template
58 tmpl_values = self.get_template_values(field, cstruct, kw)
59 return field.renderer(template, **tmpl_values)
@@ -1,25 +1,25 b''
1 <div tal:define="css_class css_class|field.widget.css_class;
1 <div tal:define="css_class css_class|field.widget.css_class;
2 style style|field.widget.style;
2 style style|field.widget.style;
3 oid oid|field.oid;
3 oid oid|field.oid;
4 inline getattr(field.widget, 'inline', False)"
4 inline getattr(field.widget, 'inline', False)"
5 tal:omit-tag="not inline">
5 tal:omit-tag="not inline">
6 ${field.start_sequence()}
6 ${field.start_sequence()}
7 <div tal:repeat="choice values | field.widget.values"
7 <div tal:repeat="choice values | field.widget.values"
8 tal:omit-tag="inline"
8 tal:omit-tag="inline"
9 class="checkbox">
9 class="checkbox">
10 <div tal:define="(value, title) choice">
10 <div tal:define="(value, title) choice">
11 <input tal:attributes="checked value in cstruct;
11 <input tal:attributes="checked value in cstruct;
12 class css_class;
12 class css_class;
13 style style"
13 style style"
14 type="checkbox"
14 type="checkbox"
15 name="checkbox"
15 name="checkbox"
16 value="${value}"
16 value="${value}"
17 id="${oid}-${repeat.choice.index}"/>
17 id="${oid}-${repeat.choice.index}"/>
18 <label for="${oid}-${repeat.choice.index}"
18 <label for="${oid}-${repeat.choice.index}"
19 tal:attributes="class inline and 'checkbox-inline'">
19 tal:attributes="class inline and 'checkbox-inline'">
20 ${title}
20 ${title}
21 </label>
21 </label>
22 </div>
22 </div>
23 </div>
23 </div>
24 ${field.end_sequence()}
24 ${field.end_sequence()}
25 </div> No newline at end of file
25 </div>
@@ -1,100 +1,100 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.model.comment import CommentsModel
25 from rhodecode.model.comment import CommentsModel
26 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.model.pull_request import PullRequestModel
27 from rhodecode.events import (
27 from rhodecode.events import (
28 PullRequestCreateEvent,
28 PullRequestCreateEvent,
29 PullRequestUpdateEvent,
29 PullRequestUpdateEvent,
30 PullRequestCommentEvent,
30 PullRequestCommentEvent,
31 PullRequestReviewEvent,
31 PullRequestReviewEvent,
32 PullRequestMergeEvent,
32 PullRequestMergeEvent,
33 PullRequestCloseEvent,
33 PullRequestCloseEvent,
34 )
34 )
35
35
36 # TODO: dan: make the serialization tests complete json comparisons
36 # TODO: dan: make the serialization tests complete json comparisons
37 @pytest.mark.backends("git", "hg")
37 @pytest.mark.backends("git", "hg")
38 @pytest.mark.parametrize('EventClass', [
38 @pytest.mark.parametrize('EventClass', [
39 PullRequestCreateEvent,
39 PullRequestCreateEvent,
40 PullRequestUpdateEvent,
40 PullRequestUpdateEvent,
41 PullRequestReviewEvent,
41 PullRequestReviewEvent,
42 PullRequestMergeEvent,
42 PullRequestMergeEvent,
43 PullRequestCloseEvent,
43 PullRequestCloseEvent
44 ])
44 ])
45 def test_pullrequest_events_serialized(EventClass, pr_util, config_stub):
45 def test_pullrequest_events_serialized(EventClass, pr_util, config_stub):
46 pr = pr_util.create_pull_request()
46 pr = pr_util.create_pull_request()
47 if EventClass == PullRequestReviewEvent:
47 if EventClass == PullRequestReviewEvent:
48 event = EventClass(pr, 'approved')
48 event = EventClass(pr, 'approved')
49 else:
49 else:
50 event = EventClass(pr)
50 event = EventClass(pr)
51 data = event.as_dict()
51 data = event.as_dict()
52 assert data['name'] == EventClass.name
52 assert data['name'] == EventClass.name
53 assert data['repo']['repo_name'] == pr.target_repo.repo_name
53 assert data['repo']['repo_name'] == pr.target_repo.repo_name
54 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
54 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
55 assert data['pullrequest']['url']
55 assert data['pullrequest']['url']
56 assert data['pullrequest']['permalink_url']
56 assert data['pullrequest']['permalink_url']
57
57
58
58
59 @pytest.mark.backends("git", "hg")
59 @pytest.mark.backends("git", "hg")
60 def test_create_pull_request_events(pr_util, config_stub):
60 def test_create_pull_request_events(pr_util, config_stub):
61 with EventCatcher() as event_catcher:
61 with EventCatcher() as event_catcher:
62 pr_util.create_pull_request()
62 pr_util.create_pull_request()
63
63
64 assert PullRequestCreateEvent in event_catcher.events_types
64 assert PullRequestCreateEvent in event_catcher.events_types
65
65
66
66
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 def test_pullrequest_comment_events_serialized(pr_util, config_stub):
68 def test_pullrequest_comment_events_serialized(pr_util, config_stub):
69 pr = pr_util.create_pull_request()
69 pr = pr_util.create_pull_request()
70 comment = CommentsModel().get_comments(
70 comment = CommentsModel().get_comments(
71 pr.target_repo.repo_id, pull_request=pr)[0]
71 pr.target_repo.repo_id, pull_request=pr)[0]
72 event = PullRequestCommentEvent(pr, comment)
72 event = PullRequestCommentEvent(pr, comment)
73 data = event.as_dict()
73 data = event.as_dict()
74 assert data['name'] == PullRequestCommentEvent.name
74 assert data['name'] == PullRequestCommentEvent.name
75 assert data['repo']['repo_name'] == pr.target_repo.repo_name
75 assert data['repo']['repo_name'] == pr.target_repo.repo_name
76 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
76 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
77 assert data['pullrequest']['url']
77 assert data['pullrequest']['url']
78 assert data['pullrequest']['permalink_url']
78 assert data['pullrequest']['permalink_url']
79 assert data['comment']['text'] == comment.text
79 assert data['comment']['text'] == comment.text
80
80
81
81
82 @pytest.mark.backends("git", "hg")
82 @pytest.mark.backends("git", "hg")
83 def test_close_pull_request_events(pr_util, user_admin, config_stub):
83 def test_close_pull_request_events(pr_util, user_admin, config_stub):
84 pr = pr_util.create_pull_request()
84 pr = pr_util.create_pull_request()
85
85
86 with EventCatcher() as event_catcher:
86 with EventCatcher() as event_catcher:
87 PullRequestModel().close_pull_request(pr, user_admin)
87 PullRequestModel().close_pull_request(pr, user_admin)
88
88
89 assert PullRequestCloseEvent in event_catcher.events_types
89 assert PullRequestCloseEvent in event_catcher.events_types
90
90
91
91
92 @pytest.mark.backends("git", "hg")
92 @pytest.mark.backends("git", "hg")
93 def test_close_pull_request_with_comment_events(pr_util, user_admin, config_stub):
93 def test_close_pull_request_with_comment_events(pr_util, user_admin, config_stub):
94 pr = pr_util.create_pull_request()
94 pr = pr_util.create_pull_request()
95
95
96 with EventCatcher() as event_catcher:
96 with EventCatcher() as event_catcher:
97 PullRequestModel().close_pull_request_with_comment(
97 PullRequestModel().close_pull_request_with_comment(
98 pr, user_admin, pr.target_repo)
98 pr, user_admin, pr.target_repo)
99
99
100 assert PullRequestCloseEvent in event_catcher.events_types
100 assert PullRequestCloseEvent in event_catcher.events_types
@@ -1,123 +1,145 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.utils2 import StrictAttributeDict
23 from rhodecode.tests.events.conftest import EventCatcher
24 from rhodecode.tests.events.conftest import EventCatcher
24
25
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
28 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
31 RepoPreCreateEvent, RepoCreateEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
32 RepoPreDeleteEvent, RepoDeleteEvent, RepoCommitCommentEvent,
32 )
33 )
33
34
34
35
35 @pytest.fixture()
36 @pytest.fixture()
36 def scm_extras(user_regular, repo_stub):
37 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
38 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
39 'ip': '127.0.0.1',
39 'username': user_regular.username,
40 'username': user_regular.username,
40 'user_id': user_regular.user_id,
41 'user_id': user_regular.user_id,
41 'action': '',
42 'action': '',
42 'repository': repo_stub.repo_name,
43 'repository': repo_stub.repo_name,
43 'scm': repo_stub.scm_instance().alias,
44 'scm': repo_stub.scm_instance().alias,
44 'config': '',
45 'config': '',
45 'repo_store': '',
46 'repo_store': '',
46 'server_url': 'http://example.com',
47 'server_url': 'http://example.com',
47 'make_lock': None,
48 'make_lock': None,
48 'user_agent': 'some-client',
49 'user_agent': 'some-client',
49 'locked_by': [None],
50 'locked_by': [None],
50 'commit_ids': ['a' * 40] * 3,
51 'commit_ids': ['a' * 40] * 3,
51 'hook_type': 'scm_extras_test',
52 'hook_type': 'scm_extras_test',
52 'is_shadow_repo': False,
53 'is_shadow_repo': False,
53 })
54 })
54 return extras
55 return extras
55
56
56
57
57 # TODO: dan: make the serialization tests complete json comparisons
58 # TODO: dan: make the serialization tests complete json comparisons
58 @pytest.mark.parametrize('EventClass', [
59 @pytest.mark.parametrize('EventClass', [
59 RepoPreCreateEvent, RepoCreateEvent,
60 RepoPreCreateEvent, RepoCreateEvent,
60 RepoPreDeleteEvent, RepoDeleteEvent,
61 RepoPreDeleteEvent, RepoDeleteEvent,
61 ])
62 ])
62 def test_repo_events_serialized(config_stub, repo_stub, EventClass):
63 def test_repo_events_serialized(config_stub, repo_stub, EventClass):
63 event = EventClass(repo_stub)
64 event = EventClass(repo_stub)
64 data = event.as_dict()
65 data = event.as_dict()
65 assert data['name'] == EventClass.name
66 assert data['name'] == EventClass.name
66 assert data['repo']['repo_name'] == repo_stub.repo_name
67 assert data['repo']['repo_name'] == repo_stub.repo_name
67 assert data['repo']['url']
68 assert data['repo']['url']
68 assert data['repo']['permalink_url']
69 assert data['repo']['permalink_url']
69
70
70
71
71 @pytest.mark.parametrize('EventClass', [
72 @pytest.mark.parametrize('EventClass', [
72 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
73 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
73 ])
74 ])
74 def test_vcs_repo_events_serialize(config_stub, repo_stub, scm_extras, EventClass):
75 def test_vcs_repo_events_serialize(config_stub, repo_stub, scm_extras, EventClass):
75 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
76 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
76 data = event.as_dict()
77 data = event.as_dict()
77 assert data['name'] == EventClass.name
78 assert data['name'] == EventClass.name
78 assert data['repo']['repo_name'] == repo_stub.repo_name
79 assert data['repo']['repo_name'] == repo_stub.repo_name
79 assert data['repo']['url']
80 assert data['repo']['url']
80 assert data['repo']['permalink_url']
81 assert data['repo']['permalink_url']
81
82
82
83
83 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
84 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
84 def test_vcs_repo_push_event_serialize(config_stub, repo_stub, scm_extras, EventClass):
85 def test_vcs_repo_push_event_serialize(config_stub, repo_stub, scm_extras, EventClass):
85 event = EventClass(repo_name=repo_stub.repo_name,
86 event = EventClass(repo_name=repo_stub.repo_name,
86 pushed_commit_ids=scm_extras['commit_ids'],
87 pushed_commit_ids=scm_extras['commit_ids'],
87 extras=scm_extras)
88 extras=scm_extras)
88 data = event.as_dict()
89 data = event.as_dict()
89 assert data['name'] == EventClass.name
90 assert data['name'] == EventClass.name
90 assert data['repo']['repo_name'] == repo_stub.repo_name
91 assert data['repo']['repo_name'] == repo_stub.repo_name
91 assert data['repo']['url']
92 assert data['repo']['url']
92 assert data['repo']['permalink_url']
93 assert data['repo']['permalink_url']
93
94
94
95
95 def test_create_delete_repo_fires_events(backend):
96 def test_create_delete_repo_fires_events(backend):
96 with EventCatcher() as event_catcher:
97 with EventCatcher() as event_catcher:
97 repo = backend.create_repo()
98 repo = backend.create_repo()
98 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
99 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
99
100
100 with EventCatcher() as event_catcher:
101 with EventCatcher() as event_catcher:
101 RepoModel().delete(repo)
102 RepoModel().delete(repo)
102 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
103 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
103
104
104
105
105 def test_pull_fires_events(scm_extras):
106 def test_pull_fires_events(scm_extras):
106 with EventCatcher() as event_catcher:
107 with EventCatcher() as event_catcher:
107 hooks_base.pre_push(scm_extras)
108 hooks_base.pre_push(scm_extras)
108 assert event_catcher.events_types == [RepoPrePushEvent]
109 assert event_catcher.events_types == [RepoPrePushEvent]
109
110
110 with EventCatcher() as event_catcher:
111 with EventCatcher() as event_catcher:
111 hooks_base.post_push(scm_extras)
112 hooks_base.post_push(scm_extras)
112 assert event_catcher.events_types == [RepoPushEvent]
113 assert event_catcher.events_types == [RepoPushEvent]
113
114
114
115
115 def test_push_fires_events(scm_extras):
116 def test_push_fires_events(scm_extras):
116 with EventCatcher() as event_catcher:
117 with EventCatcher() as event_catcher:
117 hooks_base.pre_pull(scm_extras)
118 hooks_base.pre_pull(scm_extras)
118 assert event_catcher.events_types == [RepoPrePullEvent]
119 assert event_catcher.events_types == [RepoPrePullEvent]
119
120
120 with EventCatcher() as event_catcher:
121 with EventCatcher() as event_catcher:
121 hooks_base.post_pull(scm_extras)
122 hooks_base.post_pull(scm_extras)
122 assert event_catcher.events_types == [RepoPullEvent]
123 assert event_catcher.events_types == [RepoPullEvent]
123
124
125
126 @pytest.mark.parametrize('EventClass', [RepoCommitCommentEvent])
127 def test_repo_commit_event(config_stub, repo_stub, EventClass):
128
129 commit = StrictAttributeDict({
130 'raw_id': 'raw_id',
131 'message': 'message',
132 'branch': 'branch',
133 })
134
135 comment = StrictAttributeDict({
136 'comment_id': 'comment_id',
137 'text': 'text',
138 'comment_type': 'comment_type',
139 'f_path': 'f_path',
140 'line_no': 'line_no',
141 })
142 event = EventClass(repo=repo_stub, commit=commit, comment=comment)
143 data = event.as_dict()
144 assert data['commit']['commit_id']
145 assert data['comment']['comment_id']
@@ -1,224 +1,225 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import pytest
22 import pytest
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Session, Integration
26 from rhodecode.model.db import Session, Integration
27 from rhodecode.model.integration import IntegrationModel
27 from rhodecode.model.integration import IntegrationModel
28
28
29
29
30 class TestDeleteScopesDeletesIntegrations(object):
30 class TestDeleteScopesDeletesIntegrations(object):
31 def test_delete_repo_with_integration_deletes_integration(
31 def test_delete_repo_with_integration_deletes_integration(
32 self, repo_integration_stub):
32 self, repo_integration_stub):
33
33
34 Session().delete(repo_integration_stub.repo)
34 Session().delete(repo_integration_stub.repo)
35 Session().commit()
35 Session().commit()
36 Session().expire_all()
36 Session().expire_all()
37 integration = Integration.get(repo_integration_stub.integration_id)
37 integration = Integration.get(repo_integration_stub.integration_id)
38 assert integration is None
38 assert integration is None
39
39
40 def test_delete_repo_group_with_integration_deletes_integration(
40 def test_delete_repo_group_with_integration_deletes_integration(
41 self, repogroup_integration_stub):
41 self, repogroup_integration_stub):
42
42
43 Session().delete(repogroup_integration_stub.repo_group)
43 Session().delete(repogroup_integration_stub.repo_group)
44 Session().commit()
44 Session().commit()
45 Session().expire_all()
45 Session().expire_all()
46 integration = Integration.get(repogroup_integration_stub.integration_id)
46 integration = Integration.get(repogroup_integration_stub.integration_id)
47 assert integration is None
47 assert integration is None
48
48
49
49
50 count = 1
50 count = 1
51
51
52
52 def counter():
53 def counter():
53 global count
54 global count
54 val = count
55 val = count
55 count += 1
56 count += 1
56 return '{}_{}'.format(val, time.time())
57 return '{}_{}'.format(val, time.time())
57
58
58
59
59 @pytest.fixture()
60 @pytest.fixture()
60 def integration_repos(request, StubIntegrationType, stub_integration_settings):
61 def integration_repos(request, StubIntegrationType, stub_integration_settings):
61 """
62 """
62 Create repositories and integrations for testing, and destroy them after
63 Create repositories and integrations for testing, and destroy them after
63
64
64 Structure:
65 Structure:
65 root_repo
66 root_repo
66 parent_group/
67 parent_group/
67 parent_repo
68 parent_repo
68 child_group/
69 child_group/
69 child_repo
70 child_repo
70 other_group/
71 other_group/
71 other_repo
72 other_repo
72 """
73 """
73 fixture = Fixture()
74 fixture = Fixture()
74
75
75 parent_group_id = 'int_test_parent_group_{}'.format(counter())
76 parent_group_id = 'int_test_parent_group_{}'.format(counter())
76 parent_group = fixture.create_repo_group(parent_group_id)
77 parent_group = fixture.create_repo_group(parent_group_id)
77
78
78 other_group_id = 'int_test_other_group_{}'.format(counter())
79 other_group_id = 'int_test_other_group_{}'.format(counter())
79 other_group = fixture.create_repo_group(other_group_id)
80 other_group = fixture.create_repo_group(other_group_id)
80
81
81 child_group_id = (
82 child_group_id = (
82 parent_group_id + '/' + 'int_test_child_group_{}'.format(counter()))
83 parent_group_id + '/' + 'int_test_child_group_{}'.format(counter()))
83 child_group = fixture.create_repo_group(child_group_id)
84 child_group = fixture.create_repo_group(child_group_id)
84
85
85 parent_repo_id = 'int_test_parent_repo_{}'.format(counter())
86 parent_repo_id = 'int_test_parent_repo_{}'.format(counter())
86 parent_repo = fixture.create_repo(parent_repo_id, repo_group=parent_group)
87 parent_repo = fixture.create_repo(parent_repo_id, repo_group=parent_group)
87
88
88 child_repo_id = 'int_test_child_repo_{}'.format(counter())
89 child_repo_id = 'int_test_child_repo_{}'.format(counter())
89 child_repo = fixture.create_repo(child_repo_id, repo_group=child_group)
90 child_repo = fixture.create_repo(child_repo_id, repo_group=child_group)
90
91
91 other_repo_id = 'int_test_other_repo_{}'.format(counter())
92 other_repo_id = 'int_test_other_repo_{}'.format(counter())
92 other_repo = fixture.create_repo(other_repo_id, repo_group=other_group)
93 other_repo = fixture.create_repo(other_repo_id, repo_group=other_group)
93
94
94 root_repo_id = 'int_test_repo_root_{}'.format(counter())
95 root_repo_id = 'int_test_repo_root_{}'.format(counter())
95 root_repo = fixture.create_repo(root_repo_id)
96 root_repo = fixture.create_repo(root_repo_id)
96
97
97 integrations = {}
98 integrations = {}
98 for name, repo, repo_group, child_repos_only in [
99 for name, repo, repo_group, child_repos_only in [
99 ('global', None, None, None),
100 ('global', None, None, None),
100 ('root_repos', None, None, True),
101 ('root_repos', None, None, True),
101 ('parent_repo', parent_repo, None, None),
102 ('parent_repo', parent_repo, None, None),
102 ('child_repo', child_repo, None, None),
103 ('child_repo', child_repo, None, None),
103 ('other_repo', other_repo, None, None),
104 ('other_repo', other_repo, None, None),
104 ('root_repo', root_repo, None, None),
105 ('root_repo', root_repo, None, None),
105 ('parent_group', None, parent_group, True),
106 ('parent_group', None, parent_group, True),
106 ('parent_group_recursive', None, parent_group, False),
107 ('parent_group_recursive', None, parent_group, False),
107 ('child_group', None, child_group, True),
108 ('child_group', None, child_group, True),
108 ('child_group_recursive', None, child_group, False),
109 ('child_group_recursive', None, child_group, False),
109 ('other_group', None, other_group, True),
110 ('other_group', None, other_group, True),
110 ('other_group_recursive', None, other_group, False),
111 ('other_group_recursive', None, other_group, False),
111 ]:
112 ]:
112 integrations[name] = IntegrationModel().create(
113 integrations[name] = IntegrationModel().create(
113 StubIntegrationType, settings=stub_integration_settings,
114 StubIntegrationType, settings=stub_integration_settings,
114 enabled=True, name='test %s integration' % name,
115 enabled=True, name='test %s integration' % name,
115 repo=repo, repo_group=repo_group, child_repos_only=child_repos_only)
116 repo=repo, repo_group=repo_group, child_repos_only=child_repos_only)
116
117
117 Session().commit()
118 Session().commit()
118
119
119 def _cleanup():
120 def _cleanup():
120 for integration in integrations.values():
121 for integration in integrations.values():
121 Session.delete(integration)
122 Session.delete(integration)
122
123
123 fixture.destroy_repo(root_repo)
124 fixture.destroy_repo(root_repo)
124 fixture.destroy_repo(child_repo)
125 fixture.destroy_repo(child_repo)
125 fixture.destroy_repo(parent_repo)
126 fixture.destroy_repo(parent_repo)
126 fixture.destroy_repo(other_repo)
127 fixture.destroy_repo(other_repo)
127 fixture.destroy_repo_group(child_group)
128 fixture.destroy_repo_group(child_group)
128 fixture.destroy_repo_group(parent_group)
129 fixture.destroy_repo_group(parent_group)
129 fixture.destroy_repo_group(other_group)
130 fixture.destroy_repo_group(other_group)
130
131
131 request.addfinalizer(_cleanup)
132 request.addfinalizer(_cleanup)
132
133
133 return {
134 return {
134 'integrations': integrations,
135 'integrations': integrations,
135 'repos': {
136 'repos': {
136 'root_repo': root_repo,
137 'root_repo': root_repo,
137 'other_repo': other_repo,
138 'other_repo': other_repo,
138 'parent_repo': parent_repo,
139 'parent_repo': parent_repo,
139 'child_repo': child_repo,
140 'child_repo': child_repo,
140 }
141 }
141 }
142 }
142
143
143
144
144 def test_enabled_integration_repo_scopes(integration_repos):
145 def test_enabled_integration_repo_scopes(integration_repos):
145 integrations = integration_repos['integrations']
146 integrations = integration_repos['integrations']
146 repos = integration_repos['repos']
147 repos = integration_repos['repos']
147
148
148 triggered_integrations = IntegrationModel().get_for_event(
149 triggered_integrations = IntegrationModel().get_for_event(
149 events.RepoEvent(repos['root_repo']))
150 events.RepoEvent(repos['root_repo']))
150
151
151 assert triggered_integrations == [
152 assert triggered_integrations == [
152 integrations['global'],
153 integrations['global'],
153 integrations['root_repos'],
154 integrations['root_repos'],
154 integrations['root_repo'],
155 integrations['root_repo'],
155 ]
156 ]
156
157
157 triggered_integrations = IntegrationModel().get_for_event(
158 triggered_integrations = IntegrationModel().get_for_event(
158 events.RepoEvent(repos['other_repo']))
159 events.RepoEvent(repos['other_repo']))
159
160
160 assert triggered_integrations == [
161 assert triggered_integrations == [
161 integrations['global'],
162 integrations['global'],
162 integrations['other_group'],
163 integrations['other_group'],
163 integrations['other_group_recursive'],
164 integrations['other_group_recursive'],
164 integrations['other_repo'],
165 integrations['other_repo'],
165 ]
166 ]
166
167
167 triggered_integrations = IntegrationModel().get_for_event(
168 triggered_integrations = IntegrationModel().get_for_event(
168 events.RepoEvent(repos['parent_repo']))
169 events.RepoEvent(repos['parent_repo']))
169
170
170 assert triggered_integrations == [
171 assert triggered_integrations == [
171 integrations['global'],
172 integrations['global'],
172 integrations['parent_group'],
173 integrations['parent_group'],
173 integrations['parent_group_recursive'],
174 integrations['parent_group_recursive'],
174 integrations['parent_repo'],
175 integrations['parent_repo'],
175 ]
176 ]
176
177
177 triggered_integrations = IntegrationModel().get_for_event(
178 triggered_integrations = IntegrationModel().get_for_event(
178 events.RepoEvent(repos['child_repo']))
179 events.RepoEvent(repos['child_repo']))
179
180
180 assert triggered_integrations == [
181 assert triggered_integrations == [
181 integrations['global'],
182 integrations['global'],
182 integrations['child_group'],
183 integrations['child_group'],
183 integrations['parent_group_recursive'],
184 integrations['parent_group_recursive'],
184 integrations['child_group_recursive'],
185 integrations['child_group_recursive'],
185 integrations['child_repo'],
186 integrations['child_repo'],
186 ]
187 ]
187
188
188
189
189 def test_disabled_integration_repo_scopes(integration_repos):
190 def test_disabled_integration_repo_scopes(integration_repos):
190 integrations = integration_repos['integrations']
191 integrations = integration_repos['integrations']
191 repos = integration_repos['repos']
192 repos = integration_repos['repos']
192
193
193 for integration in integrations.values():
194 for integration in integrations.values():
194 integration.enabled = False
195 integration.enabled = False
195 Session().commit()
196 Session().commit()
196
197
197 triggered_integrations = IntegrationModel().get_for_event(
198 triggered_integrations = IntegrationModel().get_for_event(
198 events.RepoEvent(repos['root_repo']))
199 events.RepoEvent(repos['root_repo']))
199
200
200 assert triggered_integrations == []
201 assert triggered_integrations == []
201
202
202 triggered_integrations = IntegrationModel().get_for_event(
203 triggered_integrations = IntegrationModel().get_for_event(
203 events.RepoEvent(repos['parent_repo']))
204 events.RepoEvent(repos['parent_repo']))
204
205
205 assert triggered_integrations == []
206 assert triggered_integrations == []
206
207
207 triggered_integrations = IntegrationModel().get_for_event(
208 triggered_integrations = IntegrationModel().get_for_event(
208 events.RepoEvent(repos['child_repo']))
209 events.RepoEvent(repos['child_repo']))
209
210
210 assert triggered_integrations == []
211 assert triggered_integrations == []
211
212
212 triggered_integrations = IntegrationModel().get_for_event(
213 triggered_integrations = IntegrationModel().get_for_event(
213 events.RepoEvent(repos['other_repo']))
214 events.RepoEvent(repos['other_repo']))
214
215
215 assert triggered_integrations == []
216 assert triggered_integrations == []
216
217
217
218
218 def test_enabled_non_repo_integrations(integration_repos):
219 def test_enabled_non_repo_integrations(integration_repos):
219 integrations = integration_repos['integrations']
220 integrations = integration_repos['integrations']
220
221
221 triggered_integrations = IntegrationModel().get_for_event(
222 triggered_integrations = IntegrationModel().get_for_event(
222 events.UserPreCreate({}))
223 events.UserPreCreate({}))
223
224
224 assert triggered_integrations == [integrations['global']]
225 assert triggered_integrations == [integrations['global']]
@@ -1,135 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode import events
23 from rhodecode import events
24 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.integrations.types.webhook import WebhookDataHandler
25 from rhodecode.integrations.types.webhook import WebhookDataHandler
26
26
27
27
28 @pytest.fixture()
28 @pytest.fixture()
29 def base_data():
29 def base_data():
30 return {
30 return {
31 'name': 'event',
31 'name': 'event',
32 'repo': {
32 'repo': {
33 'repo_name': 'foo',
33 'repo_name': 'foo',
34 'repo_type': 'hg',
34 'repo_type': 'hg',
35 'repo_id': '12',
35 'repo_id': '12',
36 'url': 'http://repo.url/foo',
36 'url': 'http://repo.url/foo',
37 'extra_fields': {},
37 'extra_fields': {},
38 },
38 },
39 'actor': {
39 'actor': {
40 'username': 'actor_name',
40 'username': 'actor_name',
41 'user_id': 1
41 'user_id': 1
42 }
42 }
43 }
43 }
44
44
45
45
46 def test_webhook_parse_url_invalid_event():
46 def test_webhook_parse_url_invalid_event():
47 template_url = 'http://server.com/${repo_name}/build'
47 template_url = 'http://server.com/${repo_name}/build'
48 handler = WebhookDataHandler(
48 handler = WebhookDataHandler(
49 template_url, {'exmaple-header': 'header-values'})
49 template_url, {'exmaple-header': 'header-values'})
50 event = events.RepoDeleteEvent('')
50 event = events.RepoDeleteEvent('')
51 with pytest.raises(ValueError) as err:
51 with pytest.raises(ValueError) as err:
52 handler(event, {})
52 handler(event, {})
53
53
54 err = str(err.value)
54 err = str(err.value)
55 assert err.startswith(
55 assert err == "event type `<class 'rhodecode.events.repo.RepoDeleteEvent'>` has no handler defined"
56 'event type `%s` not in supported list' % event.__class__)
57
56
58
57
59 @pytest.mark.parametrize('template,expected_urls', [
58 @pytest.mark.parametrize('template,expected_urls', [
60 ('http://server.com/${repo_name}/build',
59 ('http://server.com/${repo_name}/build',
61 ['http://server.com/foo/build']),
60 ['http://server.com/foo/build']),
62 ('http://server.com/${repo_name}/${repo_type}',
61 ('http://server.com/${repo_name}/${repo_type}',
63 ['http://server.com/foo/hg']),
62 ['http://server.com/foo/hg']),
64 ('http://${server}.com/${repo_name}/${repo_id}',
63 ('http://${server}.com/${repo_name}/${repo_id}',
65 ['http://${server}.com/foo/12']),
64 ['http://${server}.com/foo/12']),
66 ('http://server.com/${branch}/build',
65 ('http://server.com/${branch}/build',
67 ['http://server.com/${branch}/build']),
66 ['http://server.com/${branch}/build']),
68 ])
67 ])
69 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
68 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
70 headers = {'exmaple-header': 'header-values'}
69 headers = {'exmaple-header': 'header-values'}
71 handler = WebhookDataHandler(template, headers)
70 handler = WebhookDataHandler(template, headers)
72 urls = handler(events.RepoCreateEvent(''), base_data)
71 urls = handler(events.RepoCreateEvent(''), base_data)
73 assert urls == [
72 assert urls == [
74 (url, headers, base_data) for url in expected_urls]
73 (url, headers, base_data) for url in expected_urls]
75
74
76
75
77 @pytest.mark.parametrize('template,expected_urls', [
76 @pytest.mark.parametrize('template,expected_urls', [
78 ('http://server.com/${repo_name}/${pull_request_id}',
77 ('http://server.com/${repo_name}/${pull_request_id}',
79 ['http://server.com/foo/999']),
78 ['http://server.com/foo/999']),
80 ('http://server.com/${repo_name}/${pull_request_url}',
79 ('http://server.com/${repo_name}/${pull_request_url}',
81 ['http://server.com/foo/http%3A//pr-url.com']),
80 ['http://server.com/foo/http%3A//pr-url.com']),
82 ('http://server.com/${repo_name}/${pull_request_url}/?TITLE=${pull_request_title}',
81 ('http://server.com/${repo_name}/${pull_request_url}/?TITLE=${pull_request_title}',
83 ['http://server.com/foo/http%3A//pr-url.com/?TITLE=example-pr-title%20Ticket%20%23123']),
82 ['http://server.com/foo/http%3A//pr-url.com/?TITLE=example-pr-title%20Ticket%20%23123']),
84 ('http://server.com/${repo_name}/?SHADOW_URL=${pull_request_shadow_url}',
83 ('http://server.com/${repo_name}/?SHADOW_URL=${pull_request_shadow_url}',
85 ['http://server.com/foo/?SHADOW_URL=http%3A//pr-url.com/repository']),
84 ['http://server.com/foo/?SHADOW_URL=http%3A//pr-url.com/repository']),
86 ])
85 ])
87 def test_webook_parse_url_for_pull_request_event(base_data, template, expected_urls):
86 def test_webook_parse_url_for_pull_request_event(base_data, template, expected_urls):
88
87
89 base_data['pullrequest'] = {
88 base_data['pullrequest'] = {
90 'pull_request_id': 999,
89 'pull_request_id': 999,
91 'url': 'http://pr-url.com',
90 'url': 'http://pr-url.com',
92 'title': 'example-pr-title Ticket #123',
91 'title': 'example-pr-title Ticket #123',
93 'commits_uid': 'abcdefg1234',
92 'commits_uid': 'abcdefg1234',
94 'shadow_url': 'http://pr-url.com/repository'
93 'shadow_url': 'http://pr-url.com/repository'
95 }
94 }
96 headers = {'exmaple-header': 'header-values'}
95 headers = {'exmaple-header': 'header-values'}
97 handler = WebhookDataHandler(template, headers)
96 handler = WebhookDataHandler(template, headers)
98 urls = handler(events.PullRequestCreateEvent(
97 urls = handler(events.PullRequestCreateEvent(
99 AttributeDict({'target_repo': 'foo'})), base_data)
98 AttributeDict({'target_repo': 'foo'})), base_data)
100 assert urls == [
99 assert urls == [
101 (url, headers, base_data) for url in expected_urls]
100 (url, headers, base_data) for url in expected_urls]
102
101
103
102
104 @pytest.mark.parametrize('template,expected_urls', [
103 @pytest.mark.parametrize('template,expected_urls', [
105 ('http://server.com/${branch}/build',
104 ('http://server.com/${branch}/build',
106 ['http://server.com/stable/build',
105 ['http://server.com/stable/build',
107 'http://server.com/dev/build']),
106 'http://server.com/dev/build']),
108 ('http://server.com/${branch}/${commit_id}',
107 ('http://server.com/${branch}/${commit_id}',
109 ['http://server.com/stable/stable-xxx',
108 ['http://server.com/stable/stable-xxx',
110 'http://server.com/stable/stable-yyy',
109 'http://server.com/stable/stable-yyy',
111 'http://server.com/dev/dev-xxx',
110 'http://server.com/dev/dev-xxx',
112 'http://server.com/dev/dev-yyy']),
111 'http://server.com/dev/dev-yyy']),
113 ('http://server.com/${branch_head}',
112 ('http://server.com/${branch_head}',
114 ['http://server.com/stable-yyy',
113 ['http://server.com/stable-yyy',
115 'http://server.com/dev-yyy']),
114 'http://server.com/dev-yyy']),
116 ('http://server.com/${commit_id}',
115 ('http://server.com/${commit_id}',
117 ['http://server.com/stable-xxx',
116 ['http://server.com/stable-xxx',
118 'http://server.com/stable-yyy',
117 'http://server.com/stable-yyy',
119 'http://server.com/dev-xxx',
118 'http://server.com/dev-xxx',
120 'http://server.com/dev-yyy']),
119 'http://server.com/dev-yyy']),
121 ])
120 ])
122 def test_webook_parse_url_for_push_event(
121 def test_webook_parse_url_for_push_event(
123 baseapp, repo_push_event, base_data, template, expected_urls):
122 baseapp, repo_push_event, base_data, template, expected_urls):
124 base_data['push'] = {
123 base_data['push'] = {
125 'branches': [{'name': 'stable'}, {'name': 'dev'}],
124 'branches': [{'name': 'stable'}, {'name': 'dev'}],
126 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
125 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
127 {'branch': 'stable', 'raw_id': 'stable-yyy'},
126 {'branch': 'stable', 'raw_id': 'stable-yyy'},
128 {'branch': 'dev', 'raw_id': 'dev-xxx'},
127 {'branch': 'dev', 'raw_id': 'dev-xxx'},
129 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
128 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
130 }
129 }
131 headers = {'exmaple-header': 'header-values'}
130 headers = {'exmaple-header': 'header-values'}
132 handler = WebhookDataHandler(template, headers)
131 handler = WebhookDataHandler(template, headers)
133 urls = handler(repo_push_event, base_data)
132 urls = handler(repo_push_event, base_data)
134 assert urls == [
133 assert urls == [
135 (url, headers, base_data) for url in expected_urls]
134 (url, headers, base_data) for url in expected_urls]
General Comments 0
You need to be logged in to leave comments. Login now