##// END OF EJS Templates
pull-request-events: expose pr title and uid for commits inside....
marcink -
r2588:f44c5e92 default
parent child Browse files
Show More
@@ -1,146 +1,151 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 22 from rhodecode.events.repo import (
23 23 RepoEvent, _commits_as_dict, _issues_as_dict)
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27
28 28 class PullRequestEvent(RepoEvent):
29 29 """
30 30 Base class for pull request events.
31 31
32 32 :param pullrequest: a :class:`PullRequest` instance
33 33 """
34 34
35 35 def __init__(self, pullrequest):
36 36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
37 37 self.pullrequest = pullrequest
38 38
39 39 def as_dict(self):
40 from rhodecode.lib.utils2 import md5_safe
40 41 from rhodecode.model.pull_request import PullRequestModel
41 42 data = super(PullRequestEvent, self).as_dict()
42 43
43 44 commits = _commits_as_dict(
44 45 self,
45 46 commit_ids=self.pullrequest.revisions,
46 47 repos=[self.pullrequest.source_repo]
47 48 )
48 49 issues = _issues_as_dict(commits)
50 # calculate hashes of all commits for unique identifier of commits
51 # inside that pull request
52 commits_hash = md5_safe(':'.join(x.get('raw_id', '') for x in commits))
49 53
50 54 data.update({
51 55 'pullrequest': {
52 56 'title': self.pullrequest.title,
53 57 'issues': issues,
54 58 'pull_request_id': self.pullrequest.pull_request_id,
55 59 'url': PullRequestModel().get_url(
56 60 self.pullrequest, request=self.request),
57 61 'permalink_url': PullRequestModel().get_url(
58 62 self.pullrequest, request=self.request, permalink=True),
59 63 'shadow_url': PullRequestModel().get_shadow_clone_url(
60 64 self.pullrequest, request=self.request),
61 65 'status': self.pullrequest.calculated_review_status(),
66 'commits_uid': commits_hash,
62 67 'commits': commits,
63 68 }
64 69 })
65 70 return data
66 71
67 72
68 73 class PullRequestCreateEvent(PullRequestEvent):
69 74 """
70 75 An instance of this class is emitted as an :term:`event` after a pull
71 76 request is created.
72 77 """
73 78 name = 'pullrequest-create'
74 79 display_name = lazy_ugettext('pullrequest created')
75 80
76 81
77 82 class PullRequestCloseEvent(PullRequestEvent):
78 83 """
79 84 An instance of this class is emitted as an :term:`event` after a pull
80 85 request is closed.
81 86 """
82 87 name = 'pullrequest-close'
83 88 display_name = lazy_ugettext('pullrequest closed')
84 89
85 90
86 91 class PullRequestUpdateEvent(PullRequestEvent):
87 92 """
88 93 An instance of this class is emitted as an :term:`event` after a pull
89 94 request's commits have been updated.
90 95 """
91 96 name = 'pullrequest-update'
92 97 display_name = lazy_ugettext('pullrequest commits updated')
93 98
94 99
95 100 class PullRequestReviewEvent(PullRequestEvent):
96 101 """
97 102 An instance of this class is emitted as an :term:`event` after a pull
98 103 request review has changed.
99 104 """
100 105 name = 'pullrequest-review'
101 106 display_name = lazy_ugettext('pullrequest review changed')
102 107
103 108
104 109 class PullRequestMergeEvent(PullRequestEvent):
105 110 """
106 111 An instance of this class is emitted as an :term:`event` after a pull
107 112 request is merged.
108 113 """
109 114 name = 'pullrequest-merge'
110 115 display_name = lazy_ugettext('pullrequest merged')
111 116
112 117
113 118 class PullRequestCommentEvent(PullRequestEvent):
114 119 """
115 120 An instance of this class is emitted as an :term:`event` after a pull
116 121 request comment is created.
117 122 """
118 123 name = 'pullrequest-comment'
119 124 display_name = lazy_ugettext('pullrequest commented')
120 125
121 126 def __init__(self, pullrequest, comment):
122 127 super(PullRequestCommentEvent, self).__init__(pullrequest)
123 128 self.comment = comment
124 129
125 130 def as_dict(self):
126 131 from rhodecode.model.comment import CommentsModel
127 132 data = super(PullRequestCommentEvent, self).as_dict()
128 133
129 134 status = None
130 135 if self.comment.status_change:
131 136 status = self.comment.status_change[0].status
132 137
133 138 data.update({
134 139 'comment': {
135 140 'status': status,
136 141 'text': self.comment.text,
137 142 'type': self.comment.comment_type,
138 143 'file': self.comment.f_path,
139 144 'line': self.comment.line_no,
140 145 'url': CommentsModel().get_url(
141 146 self.comment, request=self.request),
142 147 'permalink_url': CommentsModel().get_url(
143 148 self.comment, request=self.request, permalink=True),
144 149 }
145 150 })
146 151 return data
@@ -1,282 +1,288 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import string
23 23 import collections
24 24 import logging
25 25 from rhodecode.translation import _
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 class IntegrationTypeBase(object):
31 31 """ Base class for IntegrationType plugins """
32 32 is_dummy = False
33 33 description = ''
34 34
35 35 @classmethod
36 36 def icon(cls):
37 37 return '''
38 38 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
39 39 <svg
40 40 xmlns:dc="http://purl.org/dc/elements/1.1/"
41 41 xmlns:cc="http://creativecommons.org/ns#"
42 42 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
43 43 xmlns:svg="http://www.w3.org/2000/svg"
44 44 xmlns="http://www.w3.org/2000/svg"
45 45 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
46 46 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
47 47 viewBox="0 -256 1792 1792"
48 48 id="svg3025"
49 49 version="1.1"
50 50 inkscape:version="0.48.3.1 r9886"
51 51 width="100%"
52 52 height="100%"
53 53 sodipodi:docname="cog_font_awesome.svg">
54 54 <metadata
55 55 id="metadata3035">
56 56 <rdf:RDF>
57 57 <cc:Work
58 58 rdf:about="">
59 59 <dc:format>image/svg+xml</dc:format>
60 60 <dc:type
61 61 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
62 62 </cc:Work>
63 63 </rdf:RDF>
64 64 </metadata>
65 65 <defs
66 66 id="defs3033" />
67 67 <sodipodi:namedview
68 68 pagecolor="#ffffff"
69 69 bordercolor="#666666"
70 70 borderopacity="1"
71 71 objecttolerance="10"
72 72 gridtolerance="10"
73 73 guidetolerance="10"
74 74 inkscape:pageopacity="0"
75 75 inkscape:pageshadow="2"
76 76 inkscape:window-width="640"
77 77 inkscape:window-height="480"
78 78 id="namedview3031"
79 79 showgrid="false"
80 80 inkscape:zoom="0.13169643"
81 81 inkscape:cx="896"
82 82 inkscape:cy="896"
83 83 inkscape:window-x="0"
84 84 inkscape:window-y="25"
85 85 inkscape:window-maximized="0"
86 86 inkscape:current-layer="svg3025" />
87 87 <g
88 88 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
89 89 id="g3027">
90 90 <path
91 91 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"
92 92 id="path3029"
93 93 inkscape:connector-curvature="0"
94 94 style="fill:currentColor" />
95 95 </g>
96 96 </svg>
97 97 '''
98 98
99 99 def __init__(self, settings):
100 100 """
101 101 :param settings: dict of settings to be used for the integration
102 102 """
103 103 self.settings = settings
104 104
105 105 def settings_schema(self):
106 106 """
107 107 A colander schema of settings for the integration type
108 108 """
109 109 return colander.Schema()
110 110
111 111
112 112 class EEIntegration(IntegrationTypeBase):
113 113 description = 'Integration available in RhodeCode EE edition.'
114 114 is_dummy = True
115 115
116 116 def __init__(self, name, key, settings=None):
117 117 self.display_name = name
118 118 self.key = key
119 119 super(EEIntegration, self).__init__(settings)
120 120
121 121
122 122 # Helpers #
123 123 WEBHOOK_URL_VARS = [
124 124 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
125 125 ('repo_name', 'Full name of the repository'),
126 126 ('repo_type', 'VCS type of repository'),
127 127 ('repo_id', 'Unique id of repository'),
128 128 ('repo_url', 'Repository url'),
129 129 # extra repo fields
130 130 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
131 131
132 132 # special attrs below that we handle, using multi-call
133 133 ('branch', 'Name of each brach submitted, if any.'),
134 134 ('commit_id', 'Id of each commit submitted, if any.'),
135 135
136 136 # pr events vars
137 137 ('pull_request_id', 'Unique ID of the pull request.'),
138 ('pull_request_title', 'Title of the pull request.'),
138 139 ('pull_request_url', 'Pull request url.'),
139 140 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
141 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
142 'Changes after PR update'),
140 143
141 144 # user who triggers the call
142 145 ('username', 'User who triggered the call.'),
143 146 ('user_id', 'User id who triggered the call.'),
144 147 ]
145 148
146 149 # common vars for url template used for CI plugins. Shared with webhook
147 150 CI_URL_VARS = WEBHOOK_URL_VARS
148 151
149 152
150 153 class WebhookDataHandler(object):
151 154 name = 'webhook'
152 155
153 156 def __init__(self, template_url, headers):
154 157 self.template_url = template_url
155 158 self.headers = headers
156 159
157 160 def get_base_parsed_template(self, data):
158 161 """
159 162 initially parses the passed in template with some common variables
160 163 available on ALL calls
161 164 """
162 165 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
163 166 common_vars = {
164 167 'repo_name': data['repo']['repo_name'],
165 168 'repo_type': data['repo']['repo_type'],
166 169 'repo_id': data['repo']['repo_id'],
167 170 'repo_url': data['repo']['url'],
168 171 'username': data['actor']['username'],
169 172 'user_id': data['actor']['user_id'],
170 173 'event_name': data['name']
171 174 }
172 175
173 176 extra_vars = {}
174 177 for extra_key, extra_val in data['repo']['extra_fields'].items():
175 178 extra_vars['extra__{}'.format(extra_key)] = extra_val
176 179 common_vars.update(extra_vars)
177 180
178 181 template_url = self.template_url.replace('${extra:', '${extra__')
179 182 return string.Template(template_url).safe_substitute(**common_vars)
180 183
181 184 def repo_push_event_handler(self, event, data):
182 185 url = self.get_base_parsed_template(data)
183 186 url_cals = []
184 187 branch_data = collections.OrderedDict()
185 188 for obj in data['push']['branches']:
186 189 branch_data[obj['name']] = obj
187 190
188 191 branches_commits = collections.OrderedDict()
189 192 for commit in data['push']['commits']:
190 193 if commit.get('git_ref_change'):
191 194 # special case for GIT that allows creating tags,
192 195 # deleting branches without associated commit
193 196 continue
194 197
195 198 if commit['branch'] not in branches_commits:
196 199 branch_commits = {'branch': branch_data[commit['branch']],
197 200 'commits': []}
198 201 branches_commits[commit['branch']] = branch_commits
199 202
200 203 branch_commits = branches_commits[commit['branch']]
201 204 branch_commits['commits'].append(commit)
202 205
203 206 if '${branch}' in url:
204 207 # call it multiple times, for each branch if used in variables
205 208 for branch, commit_ids in branches_commits.items():
206 209 branch_url = string.Template(url).safe_substitute(branch=branch)
207 210 # call further down for each commit if used
208 211 if '${commit_id}' in branch_url:
209 212 for commit_data in commit_ids['commits']:
210 213 commit_id = commit_data['raw_id']
211 214 commit_url = string.Template(branch_url).safe_substitute(
212 215 commit_id=commit_id)
213 216 # register per-commit call
214 217 log.debug(
215 218 'register %s call(%s) to url %s',
216 219 self.name, event, commit_url)
217 220 url_cals.append(
218 221 (commit_url, self.headers, data))
219 222
220 223 else:
221 224 # register per-branch call
222 225 log.debug(
223 226 'register %s call(%s) to url %s',
224 227 self.name, event, branch_url)
225 228 url_cals.append(
226 229 (branch_url, self.headers, data))
227 230
228 231 else:
229 232 log.debug(
230 233 'register %s call(%s) to url %s', self.name, event, url)
231 234 url_cals.append((url, self.headers, data))
232 235
233 236 return url_cals
234 237
235 238 def repo_create_event_handler(self, event, data):
236 239 url = self.get_base_parsed_template(data)
237 240 log.debug(
238 241 'register %s call(%s) to url %s', self.name, event, url)
239 242 return [(url, self.headers, data)]
240 243
241 244 def pull_request_event_handler(self, event, data):
242 245 url = self.get_base_parsed_template(data)
243 246 log.debug(
244 247 'register %s call(%s) to url %s', self.name, event, url)
245 248 url = string.Template(url).safe_substitute(
246 249 pull_request_id=data['pullrequest']['pull_request_id'],
250 pull_request_title=data['pullrequest']['title'],
247 251 pull_request_url=data['pullrequest']['url'],
248 pull_request_shadow_url=data['pullrequest']['shadow_url'],)
252 pull_request_shadow_url=data['pullrequest']['shadow_url'],
253 pull_request_commits_uid=data['pullrequest']['commits_uid'],
254 )
249 255 return [(url, self.headers, data)]
250 256
251 257 def __call__(self, event, data):
252 258 from rhodecode import events
253 259
254 260 if isinstance(event, events.RepoPushEvent):
255 261 return self.repo_push_event_handler(event, data)
256 262 elif isinstance(event, events.RepoCreateEvent):
257 263 return self.repo_create_event_handler(event, data)
258 264 elif isinstance(event, events.PullRequestEvent):
259 265 return self.pull_request_event_handler(event, data)
260 266 else:
261 267 raise ValueError(
262 268 'event type `%s` not in supported list: %s' % (
263 269 event.__class__, events))
264 270
265 271
266 272 def get_auth(settings):
267 273 from requests.auth import HTTPBasicAuth
268 274 username = settings.get('username')
269 275 password = settings.get('password')
270 276 if username and password:
271 277 return HTTPBasicAuth(username, password)
272 278 return None
273 279
274 280
275 281 def get_web_token(settings):
276 282 return settings['secret_token']
277 283
278 284
279 285 def get_url_vars(url_vars):
280 286 return '\n'.join(
281 287 '{} - {}'.format('${' + key + '}', explanation)
282 288 for key, explanation in url_vars)
@@ -1,122 +1,124 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode import events
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25 from rhodecode.integrations.types.webhook import WebhookDataHandler
26 26
27 27
28 28 @pytest.fixture
29 29 def base_data():
30 30 return {
31 31 'name': 'event',
32 32 'repo': {
33 33 'repo_name': 'foo',
34 34 'repo_type': 'hg',
35 35 'repo_id': '12',
36 36 'url': 'http://repo.url/foo',
37 37 'extra_fields': {},
38 38 },
39 39 'actor': {
40 40 'username': 'actor_name',
41 41 'user_id': 1
42 42 }
43 43 }
44 44
45 45
46 46 def test_webhook_parse_url_invalid_event():
47 47 template_url = 'http://server.com/${repo_name}/build'
48 48 handler = WebhookDataHandler(
49 49 template_url, {'exmaple-header': 'header-values'})
50 50 event = events.RepoDeleteEvent('')
51 51 with pytest.raises(ValueError) as err:
52 52 handler(event, {})
53 53
54 54 err = str(err.value)
55 55 assert err.startswith(
56 56 'event type `%s` not in supported list' % event.__class__)
57 57
58 58
59 59 @pytest.mark.parametrize('template,expected_urls', [
60 60 ('http://server.com/${repo_name}/build',
61 61 ['http://server.com/foo/build']),
62 62 ('http://server.com/${repo_name}/${repo_type}',
63 63 ['http://server.com/foo/hg']),
64 64 ('http://${server}.com/${repo_name}/${repo_id}',
65 65 ['http://${server}.com/foo/12']),
66 66 ('http://server.com/${branch}/build',
67 67 ['http://server.com/${branch}/build']),
68 68 ])
69 69 def test_webook_parse_url_for_create_event(base_data, template, expected_urls):
70 70 headers = {'exmaple-header': 'header-values'}
71 71 handler = WebhookDataHandler(template, headers)
72 72 urls = handler(events.RepoCreateEvent(''), base_data)
73 73 assert urls == [
74 74 (url, headers, base_data) for url in expected_urls]
75 75
76 76
77 77 @pytest.mark.parametrize('template,expected_urls', [
78 78 ('http://server.com/${repo_name}/${pull_request_id}',
79 79 ['http://server.com/foo/999']),
80 80 ('http://server.com/${repo_name}/${pull_request_url}',
81 81 ['http://server.com/foo/http://pr-url.com']),
82 82 ])
83 83 def test_webook_parse_url_for_pull_request_event(
84 84 base_data, template, expected_urls):
85 85
86 86 base_data['pullrequest'] = {
87 87 'pull_request_id': 999,
88 88 'url': 'http://pr-url.com',
89 'title': 'example-pr-title',
90 'commits_uid': 'abcdefg1234',
89 91 'shadow_url': 'http://pr-url.com/repository'
90 92 }
91 93 headers = {'exmaple-header': 'header-values'}
92 94 handler = WebhookDataHandler(template, headers)
93 95 urls = handler(events.PullRequestCreateEvent(
94 96 AttributeDict({'target_repo': 'foo'})), base_data)
95 97 assert urls == [
96 98 (url, headers, base_data) for url in expected_urls]
97 99
98 100
99 101 @pytest.mark.parametrize('template,expected_urls', [
100 102 ('http://server.com/${branch}/build',
101 103 ['http://server.com/stable/build',
102 104 'http://server.com/dev/build']),
103 105 ('http://server.com/${branch}/${commit_id}',
104 106 ['http://server.com/stable/stable-xxx',
105 107 'http://server.com/stable/stable-yyy',
106 108 'http://server.com/dev/dev-xxx',
107 109 'http://server.com/dev/dev-yyy']),
108 110 ])
109 111 def test_webook_parse_url_for_push_event(
110 112 baseapp, repo_push_event, base_data, template, expected_urls):
111 113 base_data['push'] = {
112 114 'branches': [{'name': 'stable'}, {'name': 'dev'}],
113 115 'commits': [{'branch': 'stable', 'raw_id': 'stable-xxx'},
114 116 {'branch': 'stable', 'raw_id': 'stable-yyy'},
115 117 {'branch': 'dev', 'raw_id': 'dev-xxx'},
116 118 {'branch': 'dev', 'raw_id': 'dev-yyy'}]
117 119 }
118 120 headers = {'exmaple-header': 'header-values'}
119 121 handler = WebhookDataHandler(template, headers)
120 122 urls = handler(repo_push_event, base_data)
121 123 assert urls == [
122 124 (url, headers, base_data) for url in expected_urls]
General Comments 0
You need to be logged in to leave comments. Login now