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