##// END OF EJS Templates
events: expose shadow repo build url.
marcink -
r2582:2d4f0f93 default
parent child Browse files
Show More
@@ -1,144 +1,146 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.model.pull_request import PullRequestModel
40 from rhodecode.model.pull_request import PullRequestModel
41 data = super(PullRequestEvent, self).as_dict()
41 data = super(PullRequestEvent, self).as_dict()
42
42
43 commits = _commits_as_dict(
43 commits = _commits_as_dict(
44 self,
44 self,
45 commit_ids=self.pullrequest.revisions,
45 commit_ids=self.pullrequest.revisions,
46 repos=[self.pullrequest.source_repo]
46 repos=[self.pullrequest.source_repo]
47 )
47 )
48 issues = _issues_as_dict(commits)
48 issues = _issues_as_dict(commits)
49
49
50 data.update({
50 data.update({
51 'pullrequest': {
51 'pullrequest': {
52 'title': self.pullrequest.title,
52 'title': self.pullrequest.title,
53 'issues': issues,
53 'issues': issues,
54 'pull_request_id': self.pullrequest.pull_request_id,
54 'pull_request_id': self.pullrequest.pull_request_id,
55 'url': PullRequestModel().get_url(
55 'url': PullRequestModel().get_url(
56 self.pullrequest, request=self.request),
56 self.pullrequest, request=self.request),
57 'permalink_url': PullRequestModel().get_url(
57 'permalink_url': PullRequestModel().get_url(
58 self.pullrequest, request=self.request, permalink=True),
58 self.pullrequest, request=self.request, permalink=True),
59 'shadow_url': PullRequestModel().get_shadow_clone_url(
60 self.pullrequest, request=self.request),
59 'status': self.pullrequest.calculated_review_status(),
61 'status': self.pullrequest.calculated_review_status(),
60 'commits': commits,
62 'commits': commits,
61 }
63 }
62 })
64 })
63 return data
65 return data
64
66
65
67
66 class PullRequestCreateEvent(PullRequestEvent):
68 class PullRequestCreateEvent(PullRequestEvent):
67 """
69 """
68 An instance of this class is emitted as an :term:`event` after a pull
70 An instance of this class is emitted as an :term:`event` after a pull
69 request is created.
71 request is created.
70 """
72 """
71 name = 'pullrequest-create'
73 name = 'pullrequest-create'
72 display_name = lazy_ugettext('pullrequest created')
74 display_name = lazy_ugettext('pullrequest created')
73
75
74
76
75 class PullRequestCloseEvent(PullRequestEvent):
77 class PullRequestCloseEvent(PullRequestEvent):
76 """
78 """
77 An instance of this class is emitted as an :term:`event` after a pull
79 An instance of this class is emitted as an :term:`event` after a pull
78 request is closed.
80 request is closed.
79 """
81 """
80 name = 'pullrequest-close'
82 name = 'pullrequest-close'
81 display_name = lazy_ugettext('pullrequest closed')
83 display_name = lazy_ugettext('pullrequest closed')
82
84
83
85
84 class PullRequestUpdateEvent(PullRequestEvent):
86 class PullRequestUpdateEvent(PullRequestEvent):
85 """
87 """
86 An instance of this class is emitted as an :term:`event` after a pull
88 An instance of this class is emitted as an :term:`event` after a pull
87 request's commits have been updated.
89 request's commits have been updated.
88 """
90 """
89 name = 'pullrequest-update'
91 name = 'pullrequest-update'
90 display_name = lazy_ugettext('pullrequest commits updated')
92 display_name = lazy_ugettext('pullrequest commits updated')
91
93
92
94
93 class PullRequestReviewEvent(PullRequestEvent):
95 class PullRequestReviewEvent(PullRequestEvent):
94 """
96 """
95 An instance of this class is emitted as an :term:`event` after a pull
97 An instance of this class is emitted as an :term:`event` after a pull
96 request review has changed.
98 request review has changed.
97 """
99 """
98 name = 'pullrequest-review'
100 name = 'pullrequest-review'
99 display_name = lazy_ugettext('pullrequest review changed')
101 display_name = lazy_ugettext('pullrequest review changed')
100
102
101
103
102 class PullRequestMergeEvent(PullRequestEvent):
104 class PullRequestMergeEvent(PullRequestEvent):
103 """
105 """
104 An instance of this class is emitted as an :term:`event` after a pull
106 An instance of this class is emitted as an :term:`event` after a pull
105 request is merged.
107 request is merged.
106 """
108 """
107 name = 'pullrequest-merge'
109 name = 'pullrequest-merge'
108 display_name = lazy_ugettext('pullrequest merged')
110 display_name = lazy_ugettext('pullrequest merged')
109
111
110
112
111 class PullRequestCommentEvent(PullRequestEvent):
113 class PullRequestCommentEvent(PullRequestEvent):
112 """
114 """
113 An instance of this class is emitted as an :term:`event` after a pull
115 An instance of this class is emitted as an :term:`event` after a pull
114 request comment is created.
116 request comment is created.
115 """
117 """
116 name = 'pullrequest-comment'
118 name = 'pullrequest-comment'
117 display_name = lazy_ugettext('pullrequest commented')
119 display_name = lazy_ugettext('pullrequest commented')
118
120
119 def __init__(self, pullrequest, comment):
121 def __init__(self, pullrequest, comment):
120 super(PullRequestCommentEvent, self).__init__(pullrequest)
122 super(PullRequestCommentEvent, self).__init__(pullrequest)
121 self.comment = comment
123 self.comment = comment
122
124
123 def as_dict(self):
125 def as_dict(self):
124 from rhodecode.model.comment import CommentsModel
126 from rhodecode.model.comment import CommentsModel
125 data = super(PullRequestCommentEvent, self).as_dict()
127 data = super(PullRequestCommentEvent, self).as_dict()
126
128
127 status = None
129 status = None
128 if self.comment.status_change:
130 if self.comment.status_change:
129 status = self.comment.status_change[0].status
131 status = self.comment.status_change[0].status
130
132
131 data.update({
133 data.update({
132 'comment': {
134 'comment': {
133 'status': status,
135 'status': status,
134 'text': self.comment.text,
136 'text': self.comment.text,
135 'type': self.comment.comment_type,
137 'type': self.comment.comment_type,
136 'file': self.comment.f_path,
138 'file': self.comment.f_path,
137 'line': self.comment.line_no,
139 'line': self.comment.line_no,
138 'url': CommentsModel().get_url(
140 'url': CommentsModel().get_url(
139 self.comment, request=self.request),
141 self.comment, request=self.request),
140 'permalink_url': CommentsModel().get_url(
142 'permalink_url': CommentsModel().get_url(
141 self.comment, request=self.request, permalink=True),
143 self.comment, request=self.request, permalink=True),
142 }
144 }
143 })
145 })
144 return data
146 return data
@@ -1,153 +1,154 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 from rhodecode.translation import _
22 from rhodecode.translation import _
23
23
24
24
25 class IntegrationTypeBase(object):
25 class IntegrationTypeBase(object):
26 """ Base class for IntegrationType plugins """
26 """ Base class for IntegrationType plugins """
27 is_dummy = False
27 is_dummy = False
28 description = ''
28 description = ''
29
29
30 @classmethod
30 @classmethod
31 def icon(cls):
31 def icon(cls):
32 return '''
32 return '''
33 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
33 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
34 <svg
34 <svg
35 xmlns:dc="http://purl.org/dc/elements/1.1/"
35 xmlns:dc="http://purl.org/dc/elements/1.1/"
36 xmlns:cc="http://creativecommons.org/ns#"
36 xmlns:cc="http://creativecommons.org/ns#"
37 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
37 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
38 xmlns:svg="http://www.w3.org/2000/svg"
38 xmlns:svg="http://www.w3.org/2000/svg"
39 xmlns="http://www.w3.org/2000/svg"
39 xmlns="http://www.w3.org/2000/svg"
40 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
40 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
41 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
41 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
42 viewBox="0 -256 1792 1792"
42 viewBox="0 -256 1792 1792"
43 id="svg3025"
43 id="svg3025"
44 version="1.1"
44 version="1.1"
45 inkscape:version="0.48.3.1 r9886"
45 inkscape:version="0.48.3.1 r9886"
46 width="100%"
46 width="100%"
47 height="100%"
47 height="100%"
48 sodipodi:docname="cog_font_awesome.svg">
48 sodipodi:docname="cog_font_awesome.svg">
49 <metadata
49 <metadata
50 id="metadata3035">
50 id="metadata3035">
51 <rdf:RDF>
51 <rdf:RDF>
52 <cc:Work
52 <cc:Work
53 rdf:about="">
53 rdf:about="">
54 <dc:format>image/svg+xml</dc:format>
54 <dc:format>image/svg+xml</dc:format>
55 <dc:type
55 <dc:type
56 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
56 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
57 </cc:Work>
57 </cc:Work>
58 </rdf:RDF>
58 </rdf:RDF>
59 </metadata>
59 </metadata>
60 <defs
60 <defs
61 id="defs3033" />
61 id="defs3033" />
62 <sodipodi:namedview
62 <sodipodi:namedview
63 pagecolor="#ffffff"
63 pagecolor="#ffffff"
64 bordercolor="#666666"
64 bordercolor="#666666"
65 borderopacity="1"
65 borderopacity="1"
66 objecttolerance="10"
66 objecttolerance="10"
67 gridtolerance="10"
67 gridtolerance="10"
68 guidetolerance="10"
68 guidetolerance="10"
69 inkscape:pageopacity="0"
69 inkscape:pageopacity="0"
70 inkscape:pageshadow="2"
70 inkscape:pageshadow="2"
71 inkscape:window-width="640"
71 inkscape:window-width="640"
72 inkscape:window-height="480"
72 inkscape:window-height="480"
73 id="namedview3031"
73 id="namedview3031"
74 showgrid="false"
74 showgrid="false"
75 inkscape:zoom="0.13169643"
75 inkscape:zoom="0.13169643"
76 inkscape:cx="896"
76 inkscape:cx="896"
77 inkscape:cy="896"
77 inkscape:cy="896"
78 inkscape:window-x="0"
78 inkscape:window-x="0"
79 inkscape:window-y="25"
79 inkscape:window-y="25"
80 inkscape:window-maximized="0"
80 inkscape:window-maximized="0"
81 inkscape:current-layer="svg3025" />
81 inkscape:current-layer="svg3025" />
82 <g
82 <g
83 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
83 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
84 id="g3027">
84 id="g3027">
85 <path
85 <path
86 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"
86 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"
87 id="path3029"
87 id="path3029"
88 inkscape:connector-curvature="0"
88 inkscape:connector-curvature="0"
89 style="fill:currentColor" />
89 style="fill:currentColor" />
90 </g>
90 </g>
91 </svg>
91 </svg>
92 '''
92 '''
93
93
94 def __init__(self, settings):
94 def __init__(self, settings):
95 """
95 """
96 :param settings: dict of settings to be used for the integration
96 :param settings: dict of settings to be used for the integration
97 """
97 """
98 self.settings = settings
98 self.settings = settings
99
99
100 def settings_schema(self):
100 def settings_schema(self):
101 """
101 """
102 A colander schema of settings for the integration type
102 A colander schema of settings for the integration type
103 """
103 """
104 return colander.Schema()
104 return colander.Schema()
105
105
106
106
107 class EEIntegration(IntegrationTypeBase):
107 class EEIntegration(IntegrationTypeBase):
108 description = 'Integration available in RhodeCode EE edition.'
108 description = 'Integration available in RhodeCode EE edition.'
109 is_dummy = True
109 is_dummy = True
110
110
111 def __init__(self, name, key, settings=None):
111 def __init__(self, name, key, settings=None):
112 self.display_name = name
112 self.display_name = name
113 self.key = key
113 self.key = key
114 super(EEIntegration, self).__init__(settings)
114 super(EEIntegration, self).__init__(settings)
115
115
116
116
117 # Helpers #
117 # Helpers #
118
118
119 # common vars for url template
119 # common vars for url template
120 CI_URL_VARS = [
120 CI_URL_VARS = [
121 'repo_name',
121 'repo_name',
122 'repo_type',
122 'repo_type',
123 'repo_id',
123 'repo_id',
124 'repo_url',
124 'repo_url',
125 # extra repo fields
125 # extra repo fields
126 'extra:<extra_key_name>',
126 'extra:<extra_key_name>',
127
127
128 # special attrs below that we handle, using multi-call
128 # special attrs below that we handle, using multi-call
129 'branch',
129 'branch',
130 'commit_id',
130 'commit_id',
131
131
132 # pr events vars
132 # pr events vars
133 'pull_request_id',
133 'pull_request_id',
134 'pull_request_url',
134 'pull_request_url',
135 'pull_request_shadow_url',
135
136
136 # user who triggers the call
137 # user who triggers the call
137 'username',
138 'username',
138 'user_id',
139 'user_id',
139
140
140 ]
141 ]
141
142
142
143
143 def get_auth(settings):
144 def get_auth(settings):
144 from requests.auth import HTTPBasicAuth
145 from requests.auth import HTTPBasicAuth
145 username = settings.get('username')
146 username = settings.get('username')
146 password = settings.get('password')
147 password = settings.get('password')
147 if username and password:
148 if username and password:
148 return HTTPBasicAuth(username, password)
149 return HTTPBasicAuth(username, password)
149 return None
150 return None
150
151
151
152
152 def get_url_vars(url_vars):
153 def get_url_vars(url_vars):
153 return ', '.join('${' + x + '}' for x in url_vars)
154 return ', '.join('${' + x + '}' for x in url_vars)
@@ -1,394 +1,396 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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import string
22 import string
23 from collections import OrderedDict
23 from collections import OrderedDict
24
24
25 import deform
25 import deform
26 import deform.widget
26 import deform.widget
27 import logging
27 import logging
28 import requests
28 import requests
29 import requests.adapters
29 import requests.adapters
30 import colander
30 import colander
31 from requests.packages.urllib3.util.retry import Retry
31 from requests.packages.urllib3.util.retry import Retry
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode import events
34 from rhodecode import events
35 from rhodecode.translation import _
35 from rhodecode.translation import _
36 from rhodecode.integrations.types.base import (
36 from rhodecode.integrations.types.base import (
37 IntegrationTypeBase, get_auth, get_url_vars)
37 IntegrationTypeBase, get_auth, get_url_vars)
38 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
38 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 # updating this required to update the `common_vars` passed in url calling func
43 # updating this required to update the `common_vars` passed in url calling func
44 WEBHOOK_URL_VARS = [
44 WEBHOOK_URL_VARS = [
45 'repo_name',
45 'repo_name',
46 'repo_type',
46 'repo_type',
47 'repo_id',
47 'repo_id',
48 'repo_url',
48 'repo_url',
49 # extra repo fields
49 # extra repo fields
50 'extra:<extra_key_name>',
50 'extra:<extra_key_name>',
51
51
52 # special attrs below that we handle, using multi-call
52 # special attrs below that we handle, using multi-call
53 'branch',
53 'branch',
54 'commit_id',
54 'commit_id',
55
55
56 # pr events vars
56 # pr events vars
57 'pull_request_id',
57 'pull_request_id',
58 'pull_request_url',
58 'pull_request_url',
59 'pull_request_shadow_url',
59
60
60 # user who triggers the call
61 # user who triggers the call
61 'username',
62 'username',
62 'user_id',
63 'user_id',
63
64
64 ]
65 ]
65 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
66 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
66
67
67
68
68 class WebhookHandler(object):
69 class WebhookHandler(object):
69 def __init__(self, template_url, secret_token, headers):
70 def __init__(self, template_url, secret_token, headers):
70 self.template_url = template_url
71 self.template_url = template_url
71 self.secret_token = secret_token
72 self.secret_token = secret_token
72 self.headers = headers
73 self.headers = headers
73
74
74 def get_base_parsed_template(self, data):
75 def get_base_parsed_template(self, data):
75 """
76 """
76 initially parses the passed in template with some common variables
77 initially parses the passed in template with some common variables
77 available on ALL calls
78 available on ALL calls
78 """
79 """
79 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
80 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
80 common_vars = {
81 common_vars = {
81 'repo_name': data['repo']['repo_name'],
82 'repo_name': data['repo']['repo_name'],
82 'repo_type': data['repo']['repo_type'],
83 'repo_type': data['repo']['repo_type'],
83 'repo_id': data['repo']['repo_id'],
84 'repo_id': data['repo']['repo_id'],
84 'repo_url': data['repo']['url'],
85 'repo_url': data['repo']['url'],
85 'username': data['actor']['username'],
86 'username': data['actor']['username'],
86 'user_id': data['actor']['user_id']
87 'user_id': data['actor']['user_id']
87 }
88 }
88
89
89 extra_vars = {}
90 extra_vars = {}
90 for extra_key, extra_val in data['repo']['extra_fields'].items():
91 for extra_key, extra_val in data['repo']['extra_fields'].items():
91 extra_vars['extra__{}'.format(extra_key)] = extra_val
92 extra_vars['extra__{}'.format(extra_key)] = extra_val
92 common_vars.update(extra_vars)
93 common_vars.update(extra_vars)
93
94
94 template_url = self.template_url.replace('${extra:', '${extra__')
95 template_url = self.template_url.replace('${extra:', '${extra__')
95 return string.Template(template_url).safe_substitute(**common_vars)
96 return string.Template(template_url).safe_substitute(**common_vars)
96
97
97 def repo_push_event_handler(self, event, data):
98 def repo_push_event_handler(self, event, data):
98 url = self.get_base_parsed_template(data)
99 url = self.get_base_parsed_template(data)
99 url_cals = []
100 url_cals = []
100 branch_data = OrderedDict()
101 branch_data = OrderedDict()
101 for obj in data['push']['branches']:
102 for obj in data['push']['branches']:
102 branch_data[obj['name']] = obj
103 branch_data[obj['name']] = obj
103
104
104 branches_commits = OrderedDict()
105 branches_commits = OrderedDict()
105 for commit in data['push']['commits']:
106 for commit in data['push']['commits']:
106 if commit.get('git_ref_change'):
107 if commit.get('git_ref_change'):
107 # special case for GIT that allows creating tags,
108 # special case for GIT that allows creating tags,
108 # deleting branches without associated commit
109 # deleting branches without associated commit
109 continue
110 continue
110
111
111 if commit['branch'] not in branches_commits:
112 if commit['branch'] not in branches_commits:
112 branch_commits = {'branch': branch_data[commit['branch']],
113 branch_commits = {'branch': branch_data[commit['branch']],
113 'commits': []}
114 'commits': []}
114 branches_commits[commit['branch']] = branch_commits
115 branches_commits[commit['branch']] = branch_commits
115
116
116 branch_commits = branches_commits[commit['branch']]
117 branch_commits = branches_commits[commit['branch']]
117 branch_commits['commits'].append(commit)
118 branch_commits['commits'].append(commit)
118
119
119 if '${branch}' in url:
120 if '${branch}' in url:
120 # call it multiple times, for each branch if used in variables
121 # call it multiple times, for each branch if used in variables
121 for branch, commit_ids in branches_commits.items():
122 for branch, commit_ids in branches_commits.items():
122 branch_url = string.Template(url).safe_substitute(branch=branch)
123 branch_url = string.Template(url).safe_substitute(branch=branch)
123 # call further down for each commit if used
124 # call further down for each commit if used
124 if '${commit_id}' in branch_url:
125 if '${commit_id}' in branch_url:
125 for commit_data in commit_ids['commits']:
126 for commit_data in commit_ids['commits']:
126 commit_id = commit_data['raw_id']
127 commit_id = commit_data['raw_id']
127 commit_url = string.Template(branch_url).safe_substitute(
128 commit_url = string.Template(branch_url).safe_substitute(
128 commit_id=commit_id)
129 commit_id=commit_id)
129 # register per-commit call
130 # register per-commit call
130 log.debug(
131 log.debug(
131 'register webhook call(%s) to url %s', event, commit_url)
132 'register webhook call(%s) to url %s', event, commit_url)
132 url_cals.append((commit_url, self.secret_token, self.headers, data))
133 url_cals.append((commit_url, self.secret_token, self.headers, data))
133
134
134 else:
135 else:
135 # register per-branch call
136 # register per-branch call
136 log.debug(
137 log.debug(
137 'register webhook call(%s) to url %s', event, branch_url)
138 'register webhook call(%s) to url %s', event, branch_url)
138 url_cals.append((branch_url, self.secret_token, self.headers, data))
139 url_cals.append((branch_url, self.secret_token, self.headers, data))
139
140
140 else:
141 else:
141 log.debug(
142 log.debug(
142 'register webhook call(%s) to url %s', event, url)
143 'register webhook call(%s) to url %s', event, url)
143 url_cals.append((url, self.secret_token, self.headers, data))
144 url_cals.append((url, self.secret_token, self.headers, data))
144
145
145 return url_cals
146 return url_cals
146
147
147 def repo_create_event_handler(self, event, data):
148 def repo_create_event_handler(self, event, data):
148 url = self.get_base_parsed_template(data)
149 url = self.get_base_parsed_template(data)
149 log.debug(
150 log.debug(
150 'register webhook call(%s) to url %s', event, url)
151 'register webhook call(%s) to url %s', event, url)
151 return [(url, self.secret_token, self.headers, data)]
152 return [(url, self.secret_token, self.headers, data)]
152
153
153 def pull_request_event_handler(self, event, data):
154 def pull_request_event_handler(self, event, data):
154 url = self.get_base_parsed_template(data)
155 url = self.get_base_parsed_template(data)
155 log.debug(
156 log.debug(
156 'register webhook call(%s) to url %s', event, url)
157 'register webhook call(%s) to url %s', event, url)
157 url = string.Template(url).safe_substitute(
158 url = string.Template(url).safe_substitute(
158 pull_request_id=data['pullrequest']['pull_request_id'],
159 pull_request_id=data['pullrequest']['pull_request_id'],
159 pull_request_url=data['pullrequest']['url'])
160 pull_request_url=data['pullrequest']['url'],
161 pull_request_shadow_url=data['pullrequest']['shadow_url'],)
160 return [(url, self.secret_token, self.headers, data)]
162 return [(url, self.secret_token, self.headers, data)]
161
163
162 def __call__(self, event, data):
164 def __call__(self, event, data):
163 if isinstance(event, events.RepoPushEvent):
165 if isinstance(event, events.RepoPushEvent):
164 return self.repo_push_event_handler(event, data)
166 return self.repo_push_event_handler(event, data)
165 elif isinstance(event, events.RepoCreateEvent):
167 elif isinstance(event, events.RepoCreateEvent):
166 return self.repo_create_event_handler(event, data)
168 return self.repo_create_event_handler(event, data)
167 elif isinstance(event, events.PullRequestEvent):
169 elif isinstance(event, events.PullRequestEvent):
168 return self.pull_request_event_handler(event, data)
170 return self.pull_request_event_handler(event, data)
169 else:
171 else:
170 raise ValueError('event type not supported: %s' % events)
172 raise ValueError('event type not supported: %s' % events)
171
173
172
174
173 class WebhookSettingsSchema(colander.Schema):
175 class WebhookSettingsSchema(colander.Schema):
174 url = colander.SchemaNode(
176 url = colander.SchemaNode(
175 colander.String(),
177 colander.String(),
176 title=_('Webhook URL'),
178 title=_('Webhook URL'),
177 description=
179 description=
178 _('URL to which Webhook should submit data. Following variables '
180 _('URL to which Webhook should submit data. Following variables '
179 'are allowed to be used: {vars}. Some of the variables would '
181 'are allowed to be used: {vars}. Some of the variables would '
180 'trigger multiple calls, like ${{branch}} or ${{commit_id}}. '
182 'trigger multiple calls, like ${{branch}} or ${{commit_id}}. '
181 'Webhook will be called as many times as unique objects in '
183 'Webhook will be called as many times as unique objects in '
182 'data in such cases.').format(vars=URL_VARS),
184 'data in such cases.').format(vars=URL_VARS),
183 missing=colander.required,
185 missing=colander.required,
184 required=True,
186 required=True,
185 validator=colander.url,
187 validator=colander.url,
186 widget=deform.widget.TextInputWidget(
188 widget=deform.widget.TextInputWidget(
187 placeholder='https://www.example.com/webhook'
189 placeholder='https://www.example.com/webhook'
188 ),
190 ),
189 )
191 )
190 secret_token = colander.SchemaNode(
192 secret_token = colander.SchemaNode(
191 colander.String(),
193 colander.String(),
192 title=_('Secret Token'),
194 title=_('Secret Token'),
193 description=_('Optional string used to validate received payloads. '
195 description=_('Optional string used to validate received payloads. '
194 'It will be sent together with event data in JSON'),
196 'It will be sent together with event data in JSON'),
195 default='',
197 default='',
196 missing='',
198 missing='',
197 widget=deform.widget.TextInputWidget(
199 widget=deform.widget.TextInputWidget(
198 placeholder='e.g. secret_token'
200 placeholder='e.g. secret_token'
199 ),
201 ),
200 )
202 )
201 username = colander.SchemaNode(
203 username = colander.SchemaNode(
202 colander.String(),
204 colander.String(),
203 title=_('Username'),
205 title=_('Username'),
204 description=_('Optional username to authenticate the call.'),
206 description=_('Optional username to authenticate the call.'),
205 default='',
207 default='',
206 missing='',
208 missing='',
207 widget=deform.widget.TextInputWidget(
209 widget=deform.widget.TextInputWidget(
208 placeholder='e.g. admin'
210 placeholder='e.g. admin'
209 ),
211 ),
210 )
212 )
211 password = colander.SchemaNode(
213 password = colander.SchemaNode(
212 colander.String(),
214 colander.String(),
213 title=_('Password'),
215 title=_('Password'),
214 description=_('Optional password to authenticate the call.'),
216 description=_('Optional password to authenticate the call.'),
215 default='',
217 default='',
216 missing='',
218 missing='',
217 widget=deform.widget.PasswordWidget(
219 widget=deform.widget.PasswordWidget(
218 placeholder='e.g. secret.',
220 placeholder='e.g. secret.',
219 redisplay=True,
221 redisplay=True,
220 ),
222 ),
221 )
223 )
222 custom_header_key = colander.SchemaNode(
224 custom_header_key = colander.SchemaNode(
223 colander.String(),
225 colander.String(),
224 title=_('Custom Header Key'),
226 title=_('Custom Header Key'),
225 description=_('Custom Header name to be set when calling endpoint.'),
227 description=_('Custom Header name to be set when calling endpoint.'),
226 default='',
228 default='',
227 missing='',
229 missing='',
228 widget=deform.widget.TextInputWidget(
230 widget=deform.widget.TextInputWidget(
229 placeholder='e.g.Authorization'
231 placeholder='e.g.Authorization'
230 ),
232 ),
231 )
233 )
232 custom_header_val = colander.SchemaNode(
234 custom_header_val = colander.SchemaNode(
233 colander.String(),
235 colander.String(),
234 title=_('Custom Header Value'),
236 title=_('Custom Header Value'),
235 description=_('Custom Header value to be set when calling endpoint.'),
237 description=_('Custom Header value to be set when calling endpoint.'),
236 default='',
238 default='',
237 missing='',
239 missing='',
238 widget=deform.widget.TextInputWidget(
240 widget=deform.widget.TextInputWidget(
239 placeholder='e.g. RcLogin auth=xxxx'
241 placeholder='e.g. RcLogin auth=xxxx'
240 ),
242 ),
241 )
243 )
242 method_type = colander.SchemaNode(
244 method_type = colander.SchemaNode(
243 colander.String(),
245 colander.String(),
244 title=_('Call Method'),
246 title=_('Call Method'),
245 description=_('Select if the Webhook call should be made '
247 description=_('Select if the Webhook call should be made '
246 'with POST or GET.'),
248 'with POST or GET.'),
247 default='post',
249 default='post',
248 missing='',
250 missing='',
249 widget=deform.widget.RadioChoiceWidget(
251 widget=deform.widget.RadioChoiceWidget(
250 values=[('get', 'GET'), ('post', 'POST')],
252 values=[('get', 'GET'), ('post', 'POST')],
251 inline=True
253 inline=True
252 ),
254 ),
253 )
255 )
254
256
255
257
256 class WebhookIntegrationType(IntegrationTypeBase):
258 class WebhookIntegrationType(IntegrationTypeBase):
257 key = 'webhook'
259 key = 'webhook'
258 display_name = _('Webhook')
260 display_name = _('Webhook')
259 description = _('Post json events to a Webhook endpoint')
261 description = _('Post json events to a Webhook endpoint')
260
262
261 @classmethod
263 @classmethod
262 def icon(cls):
264 def icon(cls):
263 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>'''
265 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>'''
264
266
265 valid_events = [
267 valid_events = [
266 events.PullRequestCloseEvent,
268 events.PullRequestCloseEvent,
267 events.PullRequestMergeEvent,
269 events.PullRequestMergeEvent,
268 events.PullRequestUpdateEvent,
270 events.PullRequestUpdateEvent,
269 events.PullRequestCommentEvent,
271 events.PullRequestCommentEvent,
270 events.PullRequestReviewEvent,
272 events.PullRequestReviewEvent,
271 events.PullRequestCreateEvent,
273 events.PullRequestCreateEvent,
272 events.RepoPushEvent,
274 events.RepoPushEvent,
273 events.RepoCreateEvent,
275 events.RepoCreateEvent,
274 ]
276 ]
275
277
276 def settings_schema(self):
278 def settings_schema(self):
277 schema = WebhookSettingsSchema()
279 schema = WebhookSettingsSchema()
278 schema.add(colander.SchemaNode(
280 schema.add(colander.SchemaNode(
279 colander.Set(),
281 colander.Set(),
280 widget=deform.widget.CheckboxChoiceWidget(
282 widget=deform.widget.CheckboxChoiceWidget(
281 values=sorted(
283 values=sorted(
282 [(e.name, e.display_name) for e in self.valid_events]
284 [(e.name, e.display_name) for e in self.valid_events]
283 )
285 )
284 ),
286 ),
285 description="Events activated for this integration",
287 description="Events activated for this integration",
286 name='events'
288 name='events'
287 ))
289 ))
288 return schema
290 return schema
289
291
290 def send_event(self, event):
292 def send_event(self, event):
291 log.debug('handling event %s with Webhook integration %s',
293 log.debug('handling event %s with Webhook integration %s',
292 event.name, self)
294 event.name, self)
293
295
294 if event.__class__ not in self.valid_events:
296 if event.__class__ not in self.valid_events:
295 log.debug('event not valid: %r' % event)
297 log.debug('event not valid: %r' % event)
296 return
298 return
297
299
298 if event.name not in self.settings['events']:
300 if event.name not in self.settings['events']:
299 log.debug('event ignored: %r' % event)
301 log.debug('event ignored: %r' % event)
300 return
302 return
301
303
302 data = event.as_dict()
304 data = event.as_dict()
303 template_url = self.settings['url']
305 template_url = self.settings['url']
304
306
305 headers = {}
307 headers = {}
306 head_key = self.settings.get('custom_header_key')
308 head_key = self.settings.get('custom_header_key')
307 head_val = self.settings.get('custom_header_val')
309 head_val = self.settings.get('custom_header_val')
308 if head_key and head_val:
310 if head_key and head_val:
309 headers = {head_key: head_val}
311 headers = {head_key: head_val}
310
312
311 handler = WebhookHandler(
313 handler = WebhookHandler(
312 template_url, self.settings['secret_token'], headers)
314 template_url, self.settings['secret_token'], headers)
313
315
314 url_calls = handler(event, data)
316 url_calls = handler(event, data)
315 log.debug('webhook: calling following urls: %s',
317 log.debug('webhook: calling following urls: %s',
316 [x[0] for x in url_calls])
318 [x[0] for x in url_calls])
317
319
318 run_task(post_to_webhook, url_calls, self.settings)
320 run_task(post_to_webhook, url_calls, self.settings)
319
321
320
322
321 @async_task(ignore_result=True, base=RequestContextTask)
323 @async_task(ignore_result=True, base=RequestContextTask)
322 def post_to_webhook(url_calls, settings):
324 def post_to_webhook(url_calls, settings):
323 """
325 """
324 Example data::
326 Example data::
325
327
326 {'actor': {'user_id': 2, 'username': u'admin'},
328 {'actor': {'user_id': 2, 'username': u'admin'},
327 'actor_ip': u'192.168.157.1',
329 'actor_ip': u'192.168.157.1',
328 'name': 'repo-push',
330 'name': 'repo-push',
329 'push': {'branches': [{'name': u'default',
331 'push': {'branches': [{'name': u'default',
330 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
332 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
331 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
333 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
332 'branch': u'default',
334 'branch': u'default',
333 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
335 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
334 'issues': [],
336 'issues': [],
335 'mentions': [],
337 'mentions': [],
336 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
338 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
337 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
339 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
338 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
340 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
339 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
341 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
340 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
342 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
341 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
343 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
342 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
344 'refs': {'bookmarks': [], 'branches': [u'default'], 'tags': [u'tip']},
343 'reviewers': [],
345 'reviewers': [],
344 'revision': 9L,
346 'revision': 9L,
345 'short_id': 'a815cc738b96',
347 'short_id': 'a815cc738b96',
346 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
348 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
347 'issues': {}},
349 'issues': {}},
348 'repo': {'extra_fields': '',
350 'repo': {'extra_fields': '',
349 'permalink_url': u'http://rc.local:8080/_7',
351 'permalink_url': u'http://rc.local:8080/_7',
350 'repo_id': 7,
352 'repo_id': 7,
351 'repo_name': u'hg-repo',
353 'repo_name': u'hg-repo',
352 'repo_type': u'hg',
354 'repo_type': u'hg',
353 'url': u'http://rc.local:8080/hg-repo'},
355 'url': u'http://rc.local:8080/hg-repo'},
354 'server_url': u'http://rc.local:8080',
356 'server_url': u'http://rc.local:8080',
355 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
357 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
356
358
357 """
359 """
358 max_retries = 3
360 max_retries = 3
359 retries = Retry(
361 retries = Retry(
360 total=max_retries,
362 total=max_retries,
361 backoff_factor=0.15,
363 backoff_factor=0.15,
362 status_forcelist=[500, 502, 503, 504])
364 status_forcelist=[500, 502, 503, 504])
363 call_headers = {
365 call_headers = {
364 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
366 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(
365 rhodecode.__version__)
367 rhodecode.__version__)
366 } # updated below with custom ones, allows override
368 } # updated below with custom ones, allows override
367
369
370 auth = get_auth(settings)
368 for url, token, headers, data in url_calls:
371 for url, token, headers, data in url_calls:
369 req_session = requests.Session()
372 req_session = requests.Session()
370 req_session.mount( # retry max N times
373 req_session.mount( # retry max N times
371 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
374 'http://', requests.adapters.HTTPAdapter(max_retries=retries))
372
375
373 method = settings.get('method_type') or 'post'
376 method = settings.get('method_type') or 'post'
374 call_method = getattr(req_session, method)
377 call_method = getattr(req_session, method)
375
378
376 headers = headers or {}
379 headers = headers or {}
377 call_headers.update(headers)
380 call_headers.update(headers)
378 auth = get_auth(settings)
379
381
380 log.debug('calling Webhook with method: %s, and auth:%s',
382 log.debug('calling Webhook with method: %s, and auth:%s',
381 call_method, auth)
383 call_method, auth)
382 if settings.get('log_data'):
384 if settings.get('log_data'):
383 log.debug('calling webhook with data: %s', data)
385 log.debug('calling webhook with data: %s', data)
384 resp = call_method(url, json={
386 resp = call_method(url, json={
385 'token': token,
387 'token': token,
386 'event': data
388 'event': data
387 }, headers=call_headers, auth=auth)
389 }, headers=call_headers, auth=auth)
388 log.debug('Got Webhook response: %s', resp)
390 log.debug('Got Webhook response: %s', resp)
389
391
390 try:
392 try:
391 resp.raise_for_status() # raise exception on a failed request
393 resp.raise_for_status() # raise exception on a failed request
392 except Exception:
394 except Exception:
393 log.error(resp.text)
395 log.error(resp.text)
394 raise
396 raise
@@ -1,1681 +1,1681 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
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
169 return self.check_user_read(pull_request, user) or owner
170
170
171 def get(self, pull_request):
171 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
173
173
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
176 order_dir='desc'):
176 order_dir='desc'):
177 repo = None
177 repo = None
178 if repo_name:
178 if repo_name:
179 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
180
180
181 q = PullRequest.query()
181 q = PullRequest.query()
182
182
183 # source or target
183 # source or target
184 if repo and source:
184 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
186 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
188
188
189 # closed,opened
189 # closed,opened
190 if statuses:
190 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
192
192
193 # opened by filter
193 # opened by filter
194 if opened_by:
194 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
196
196
197 if order_by:
197 if order_by:
198 order_map = {
198 order_map = {
199 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
200 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
203 }
203 }
204 if order_dir == 'asc':
204 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
206 else:
206 else:
207 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
208
208
209 return q
209 return q
210
210
211 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
212 opened_by=None):
213 """
213 """
214 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
215
215
216 :param repo_name: target or source repo
216 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
220 :returns: int number of pull requests
221 """
221 """
222 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224
224
225 return q.count()
225 return q.count()
226
226
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
229 """
230 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
231
231
232 :param repo_name: target or source repo
232 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
236 :param offset: pagination offset
236 :param offset: pagination offset
237 :param length: length of returned list
237 :param length: length of returned list
238 :param order_by: order of the returned list
238 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
240 :returns: list of pull requests
241 """
241 """
242 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
245
245
246 if length:
246 if length:
247 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
248 else:
248 else:
249 pull_requests = q.all()
249 pull_requests = q.all()
250
250
251 return pull_requests
251 return pull_requests
252
252
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
254 opened_by=None):
255 """
255 """
256 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
257 awaiting review.
257 awaiting review.
258
258
259 :param repo_name: target or source repo
259 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
263 :returns: int number of pull requests
264 """
264 """
265 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review.
275 review.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _filtered_pull_requests = []
291 _filtered_pull_requests = []
292 for pr in pull_requests:
292 for pr in pull_requests:
293 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
304 """
304 """
305 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
306 awaiting review from a specific user.
307
307
308 :param repo_name: target or source repo
308 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
313 :returns: int number of pull requests
313 :returns: int number of pull requests
314 """
314 """
315 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
317 user_id=user_id)
318
318
319 return len(pull_requests)
319 return len(pull_requests)
320
320
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
324 """
324 """
325 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
326 review from a specific user.
327
327
328 :param repo_name: target or source repo
328 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
333 :param offset: pagination offset
334 :param length: length of returned list
334 :param length: length of returned list
335 :param order_by: order of the returned list
335 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
337 :returns: list of pull requests
338 """
338 """
339 pull_requests = self.get_all(
339 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
342
342
343 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
344 my_participation = []
345 for pr in pull_requests:
345 for pr in pull_requests:
346 if pr in _my:
346 if pr in _my:
347 my_participation.append(pr)
347 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
349 if length:
349 if length:
350 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
351 else:
351 else:
352 return _filtered_pull_requests
352 return _filtered_pull_requests
353
353
354 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
355 return [
355 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
358 ]
358 ]
359
359
360 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
362 q = PullRequest.query()
363 if user_id:
363 if user_id:
364 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
367 user_filter = or_(
368 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
370 )
371 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
372
372
373 # closed,opened
373 # closed,opened
374 if statuses:
374 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
376
376
377 if order_by:
377 if order_by:
378 order_map = {
378 order_map = {
379 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
380 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
383 }
383 }
384 if order_dir == 'asc':
384 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
386 else:
386 else:
387 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
388
388
389 return q
389 return q
390
390
391 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
393 return q.count()
394
394
395 def get_im_participating_in(
395 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
398 """
398 """
399 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
400 """
400 """
401
401
402 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
404 order_dir=order_dir)
405
405
406 if length:
406 if length:
407 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
408 else:
408 else:
409 pull_requests = q.all()
409 pull_requests = q.all()
410
410
411 return pull_requests
411 return pull_requests
412
412
413 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
414 """
414 """
415 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
416 """
416 """
417 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
420 .all()
421
421
422 def get_pr_version(self, pull_request_id, version=None):
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
423 at_version = None
424
424
425 if version and version == 'latest':
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
429 at_version = 'latest'
430 elif version:
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
434 at_version = pull_request_ver.pull_request_version_id
435 else:
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
437 pull_request_id)
438
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
440 pull_request_obj, _org_pull_request_obj)
441
441
442 return _org_pull_request_obj, pull_request_obj, \
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
443 pull_request_display_obj, at_version
444
444
445 def create(self, created_by, source_repo, source_ref, target_repo,
445 def create(self, created_by, source_repo, source_ref, target_repo,
446 target_ref, revisions, reviewers, title, description=None,
446 target_ref, revisions, reviewers, title, description=None,
447 reviewer_data=None, translator=None):
447 reviewer_data=None, translator=None):
448 translator = translator or get_current_request().translate
448 translator = translator or get_current_request().translate
449
449
450 created_by_user = self._get_user(created_by)
450 created_by_user = self._get_user(created_by)
451 source_repo = self._get_repo(source_repo)
451 source_repo = self._get_repo(source_repo)
452 target_repo = self._get_repo(target_repo)
452 target_repo = self._get_repo(target_repo)
453
453
454 pull_request = PullRequest()
454 pull_request = PullRequest()
455 pull_request.source_repo = source_repo
455 pull_request.source_repo = source_repo
456 pull_request.source_ref = source_ref
456 pull_request.source_ref = source_ref
457 pull_request.target_repo = target_repo
457 pull_request.target_repo = target_repo
458 pull_request.target_ref = target_ref
458 pull_request.target_ref = target_ref
459 pull_request.revisions = revisions
459 pull_request.revisions = revisions
460 pull_request.title = title
460 pull_request.title = title
461 pull_request.description = description
461 pull_request.description = description
462 pull_request.author = created_by_user
462 pull_request.author = created_by_user
463 pull_request.reviewer_data = reviewer_data
463 pull_request.reviewer_data = reviewer_data
464
464
465 Session().add(pull_request)
465 Session().add(pull_request)
466 Session().flush()
466 Session().flush()
467
467
468 reviewer_ids = set()
468 reviewer_ids = set()
469 # members / reviewers
469 # members / reviewers
470 for reviewer_object in reviewers:
470 for reviewer_object in reviewers:
471 user_id, reasons, mandatory, rules = reviewer_object
471 user_id, reasons, mandatory, rules = reviewer_object
472 user = self._get_user(user_id)
472 user = self._get_user(user_id)
473
473
474 # skip duplicates
474 # skip duplicates
475 if user.user_id in reviewer_ids:
475 if user.user_id in reviewer_ids:
476 continue
476 continue
477
477
478 reviewer_ids.add(user.user_id)
478 reviewer_ids.add(user.user_id)
479
479
480 reviewer = PullRequestReviewers()
480 reviewer = PullRequestReviewers()
481 reviewer.user = user
481 reviewer.user = user
482 reviewer.pull_request = pull_request
482 reviewer.pull_request = pull_request
483 reviewer.reasons = reasons
483 reviewer.reasons = reasons
484 reviewer.mandatory = mandatory
484 reviewer.mandatory = mandatory
485
485
486 # NOTE(marcink): pick only first rule for now
486 # NOTE(marcink): pick only first rule for now
487 rule_id = rules[0] if rules else None
487 rule_id = rules[0] if rules else None
488 rule = RepoReviewRule.get(rule_id) if rule_id else None
488 rule = RepoReviewRule.get(rule_id) if rule_id else None
489 if rule:
489 if rule:
490 review_group = rule.user_group_vote_rule()
490 review_group = rule.user_group_vote_rule()
491 if review_group:
491 if review_group:
492 # NOTE(marcink):
492 # NOTE(marcink):
493 # again, can be that user is member of more,
493 # again, can be that user is member of more,
494 # but we pick the first same, as default reviewers algo
494 # but we pick the first same, as default reviewers algo
495 review_group = review_group[0]
495 review_group = review_group[0]
496
496
497 rule_data = {
497 rule_data = {
498 'rule_name':
498 'rule_name':
499 rule.review_rule_name,
499 rule.review_rule_name,
500 'rule_user_group_entry_id':
500 'rule_user_group_entry_id':
501 review_group.repo_review_rule_users_group_id,
501 review_group.repo_review_rule_users_group_id,
502 'rule_user_group_name':
502 'rule_user_group_name':
503 review_group.users_group.users_group_name,
503 review_group.users_group.users_group_name,
504 'rule_user_group_members':
504 'rule_user_group_members':
505 [x.user.username for x in review_group.users_group.members],
505 [x.user.username for x in review_group.users_group.members],
506 }
506 }
507 # e.g {'vote_rule': -1, 'mandatory': True}
507 # e.g {'vote_rule': -1, 'mandatory': True}
508 rule_data.update(review_group.rule_data())
508 rule_data.update(review_group.rule_data())
509
509
510 reviewer.rule_data = rule_data
510 reviewer.rule_data = rule_data
511
511
512 Session().add(reviewer)
512 Session().add(reviewer)
513
513
514 # Set approval status to "Under Review" for all commits which are
514 # Set approval status to "Under Review" for all commits which are
515 # part of this pull request.
515 # part of this pull request.
516 ChangesetStatusModel().set_status(
516 ChangesetStatusModel().set_status(
517 repo=target_repo,
517 repo=target_repo,
518 status=ChangesetStatus.STATUS_UNDER_REVIEW,
518 status=ChangesetStatus.STATUS_UNDER_REVIEW,
519 user=created_by_user,
519 user=created_by_user,
520 pull_request=pull_request
520 pull_request=pull_request
521 )
521 )
522
522
523 MergeCheck.validate(
523 MergeCheck.validate(
524 pull_request, user=created_by_user, translator=translator)
524 pull_request, user=created_by_user, translator=translator)
525
525
526 self.notify_reviewers(pull_request, reviewer_ids)
526 self.notify_reviewers(pull_request, reviewer_ids)
527 self._trigger_pull_request_hook(
527 self._trigger_pull_request_hook(
528 pull_request, created_by_user, 'create')
528 pull_request, created_by_user, 'create')
529
529
530 creation_data = pull_request.get_api_data(with_merge_state=False)
530 creation_data = pull_request.get_api_data(with_merge_state=False)
531 self._log_audit_action(
531 self._log_audit_action(
532 'repo.pull_request.create', {'data': creation_data},
532 'repo.pull_request.create', {'data': creation_data},
533 created_by_user, pull_request)
533 created_by_user, pull_request)
534
534
535 return pull_request
535 return pull_request
536
536
537 def _trigger_pull_request_hook(self, pull_request, user, action):
537 def _trigger_pull_request_hook(self, pull_request, user, action):
538 pull_request = self.__get_pull_request(pull_request)
538 pull_request = self.__get_pull_request(pull_request)
539 target_scm = pull_request.target_repo.scm_instance()
539 target_scm = pull_request.target_repo.scm_instance()
540 if action == 'create':
540 if action == 'create':
541 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
541 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
542 elif action == 'merge':
542 elif action == 'merge':
543 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
543 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
544 elif action == 'close':
544 elif action == 'close':
545 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
545 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
546 elif action == 'review_status_change':
546 elif action == 'review_status_change':
547 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
547 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
548 elif action == 'update':
548 elif action == 'update':
549 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
549 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
550 else:
550 else:
551 return
551 return
552
552
553 trigger_hook(
553 trigger_hook(
554 username=user.username,
554 username=user.username,
555 repo_name=pull_request.target_repo.repo_name,
555 repo_name=pull_request.target_repo.repo_name,
556 repo_alias=target_scm.alias,
556 repo_alias=target_scm.alias,
557 pull_request=pull_request)
557 pull_request=pull_request)
558
558
559 def _get_commit_ids(self, pull_request):
559 def _get_commit_ids(self, pull_request):
560 """
560 """
561 Return the commit ids of the merged pull request.
561 Return the commit ids of the merged pull request.
562
562
563 This method is not dealing correctly yet with the lack of autoupdates
563 This method is not dealing correctly yet with the lack of autoupdates
564 nor with the implicit target updates.
564 nor with the implicit target updates.
565 For example: if a commit in the source repo is already in the target it
565 For example: if a commit in the source repo is already in the target it
566 will be reported anyways.
566 will be reported anyways.
567 """
567 """
568 merge_rev = pull_request.merge_rev
568 merge_rev = pull_request.merge_rev
569 if merge_rev is None:
569 if merge_rev is None:
570 raise ValueError('This pull request was not merged yet')
570 raise ValueError('This pull request was not merged yet')
571
571
572 commit_ids = list(pull_request.revisions)
572 commit_ids = list(pull_request.revisions)
573 if merge_rev not in commit_ids:
573 if merge_rev not in commit_ids:
574 commit_ids.append(merge_rev)
574 commit_ids.append(merge_rev)
575
575
576 return commit_ids
576 return commit_ids
577
577
578 def merge(self, pull_request, user, extras):
578 def merge(self, pull_request, user, extras):
579 log.debug("Merging pull request %s", pull_request.pull_request_id)
579 log.debug("Merging pull request %s", pull_request.pull_request_id)
580 merge_state = self._merge_pull_request(pull_request, user, extras)
580 merge_state = self._merge_pull_request(pull_request, user, extras)
581 if merge_state.executed:
581 if merge_state.executed:
582 log.debug(
582 log.debug(
583 "Merge was successful, updating the pull request comments.")
583 "Merge was successful, updating the pull request comments.")
584 self._comment_and_close_pr(pull_request, user, merge_state)
584 self._comment_and_close_pr(pull_request, user, merge_state)
585
585
586 self._log_audit_action(
586 self._log_audit_action(
587 'repo.pull_request.merge',
587 'repo.pull_request.merge',
588 {'merge_state': merge_state.__dict__},
588 {'merge_state': merge_state.__dict__},
589 user, pull_request)
589 user, pull_request)
590
590
591 else:
591 else:
592 log.warn("Merge failed, not updating the pull request.")
592 log.warn("Merge failed, not updating the pull request.")
593 return merge_state
593 return merge_state
594
594
595 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
595 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
596 target_vcs = pull_request.target_repo.scm_instance()
596 target_vcs = pull_request.target_repo.scm_instance()
597 source_vcs = pull_request.source_repo.scm_instance()
597 source_vcs = pull_request.source_repo.scm_instance()
598 target_ref = self._refresh_reference(
598 target_ref = self._refresh_reference(
599 pull_request.target_ref_parts, target_vcs)
599 pull_request.target_ref_parts, target_vcs)
600
600
601 message = merge_msg or (
601 message = merge_msg or (
602 'Merge pull request #%(pr_id)s from '
602 'Merge pull request #%(pr_id)s from '
603 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
603 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
604 'pr_id': pull_request.pull_request_id,
604 'pr_id': pull_request.pull_request_id,
605 'source_repo': source_vcs.name,
605 'source_repo': source_vcs.name,
606 'source_ref_name': pull_request.source_ref_parts.name,
606 'source_ref_name': pull_request.source_ref_parts.name,
607 'pr_title': pull_request.title
607 'pr_title': pull_request.title
608 }
608 }
609
609
610 workspace_id = self._workspace_id(pull_request)
610 workspace_id = self._workspace_id(pull_request)
611 use_rebase = self._use_rebase_for_merging(pull_request)
611 use_rebase = self._use_rebase_for_merging(pull_request)
612 close_branch = self._close_branch_before_merging(pull_request)
612 close_branch = self._close_branch_before_merging(pull_request)
613
613
614 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617
617
618 with callback_daemon:
618 with callback_daemon:
619 # TODO: johbo: Implement a clean way to run a config_override
619 # TODO: johbo: Implement a clean way to run a config_override
620 # for a single call.
620 # for a single call.
621 target_vcs.config.set(
621 target_vcs.config.set(
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 merge_state = target_vcs.merge(
623 merge_state = target_vcs.merge(
624 target_ref, source_vcs, pull_request.source_ref_parts,
624 target_ref, source_vcs, pull_request.source_ref_parts,
625 workspace_id, user_name=user.username,
625 workspace_id, user_name=user.username,
626 user_email=user.email, message=message, use_rebase=use_rebase,
626 user_email=user.email, message=message, use_rebase=use_rebase,
627 close_branch=close_branch)
627 close_branch=close_branch)
628 return merge_state
628 return merge_state
629
629
630 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
630 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
631 pull_request.merge_rev = merge_state.merge_ref.commit_id
631 pull_request.merge_rev = merge_state.merge_ref.commit_id
632 pull_request.updated_on = datetime.datetime.now()
632 pull_request.updated_on = datetime.datetime.now()
633 close_msg = close_msg or 'Pull request merged and closed'
633 close_msg = close_msg or 'Pull request merged and closed'
634
634
635 CommentsModel().create(
635 CommentsModel().create(
636 text=safe_unicode(close_msg),
636 text=safe_unicode(close_msg),
637 repo=pull_request.target_repo.repo_id,
637 repo=pull_request.target_repo.repo_id,
638 user=user.user_id,
638 user=user.user_id,
639 pull_request=pull_request.pull_request_id,
639 pull_request=pull_request.pull_request_id,
640 f_path=None,
640 f_path=None,
641 line_no=None,
641 line_no=None,
642 closing_pr=True
642 closing_pr=True
643 )
643 )
644
644
645 Session().add(pull_request)
645 Session().add(pull_request)
646 Session().flush()
646 Session().flush()
647 # TODO: paris: replace invalidation with less radical solution
647 # TODO: paris: replace invalidation with less radical solution
648 ScmModel().mark_for_invalidation(
648 ScmModel().mark_for_invalidation(
649 pull_request.target_repo.repo_name)
649 pull_request.target_repo.repo_name)
650 self._trigger_pull_request_hook(pull_request, user, 'merge')
650 self._trigger_pull_request_hook(pull_request, user, 'merge')
651
651
652 def has_valid_update_type(self, pull_request):
652 def has_valid_update_type(self, pull_request):
653 source_ref_type = pull_request.source_ref_parts.type
653 source_ref_type = pull_request.source_ref_parts.type
654 return source_ref_type in ['book', 'branch', 'tag']
654 return source_ref_type in ['book', 'branch', 'tag']
655
655
656 def update_commits(self, pull_request):
656 def update_commits(self, pull_request):
657 """
657 """
658 Get the updated list of commits for the pull request
658 Get the updated list of commits for the pull request
659 and return the new pull request version and the list
659 and return the new pull request version and the list
660 of commits processed by this update action
660 of commits processed by this update action
661 """
661 """
662 pull_request = self.__get_pull_request(pull_request)
662 pull_request = self.__get_pull_request(pull_request)
663 source_ref_type = pull_request.source_ref_parts.type
663 source_ref_type = pull_request.source_ref_parts.type
664 source_ref_name = pull_request.source_ref_parts.name
664 source_ref_name = pull_request.source_ref_parts.name
665 source_ref_id = pull_request.source_ref_parts.commit_id
665 source_ref_id = pull_request.source_ref_parts.commit_id
666
666
667 target_ref_type = pull_request.target_ref_parts.type
667 target_ref_type = pull_request.target_ref_parts.type
668 target_ref_name = pull_request.target_ref_parts.name
668 target_ref_name = pull_request.target_ref_parts.name
669 target_ref_id = pull_request.target_ref_parts.commit_id
669 target_ref_id = pull_request.target_ref_parts.commit_id
670
670
671 if not self.has_valid_update_type(pull_request):
671 if not self.has_valid_update_type(pull_request):
672 log.debug(
672 log.debug(
673 "Skipping update of pull request %s due to ref type: %s",
673 "Skipping update of pull request %s due to ref type: %s",
674 pull_request, source_ref_type)
674 pull_request, source_ref_type)
675 return UpdateResponse(
675 return UpdateResponse(
676 executed=False,
676 executed=False,
677 reason=UpdateFailureReason.WRONG_REF_TYPE,
677 reason=UpdateFailureReason.WRONG_REF_TYPE,
678 old=pull_request, new=None, changes=None,
678 old=pull_request, new=None, changes=None,
679 source_changed=False, target_changed=False)
679 source_changed=False, target_changed=False)
680
680
681 # source repo
681 # source repo
682 source_repo = pull_request.source_repo.scm_instance()
682 source_repo = pull_request.source_repo.scm_instance()
683 try:
683 try:
684 source_commit = source_repo.get_commit(commit_id=source_ref_name)
684 source_commit = source_repo.get_commit(commit_id=source_ref_name)
685 except CommitDoesNotExistError:
685 except CommitDoesNotExistError:
686 return UpdateResponse(
686 return UpdateResponse(
687 executed=False,
687 executed=False,
688 reason=UpdateFailureReason.MISSING_SOURCE_REF,
688 reason=UpdateFailureReason.MISSING_SOURCE_REF,
689 old=pull_request, new=None, changes=None,
689 old=pull_request, new=None, changes=None,
690 source_changed=False, target_changed=False)
690 source_changed=False, target_changed=False)
691
691
692 source_changed = source_ref_id != source_commit.raw_id
692 source_changed = source_ref_id != source_commit.raw_id
693
693
694 # target repo
694 # target repo
695 target_repo = pull_request.target_repo.scm_instance()
695 target_repo = pull_request.target_repo.scm_instance()
696 try:
696 try:
697 target_commit = target_repo.get_commit(commit_id=target_ref_name)
697 target_commit = target_repo.get_commit(commit_id=target_ref_name)
698 except CommitDoesNotExistError:
698 except CommitDoesNotExistError:
699 return UpdateResponse(
699 return UpdateResponse(
700 executed=False,
700 executed=False,
701 reason=UpdateFailureReason.MISSING_TARGET_REF,
701 reason=UpdateFailureReason.MISSING_TARGET_REF,
702 old=pull_request, new=None, changes=None,
702 old=pull_request, new=None, changes=None,
703 source_changed=False, target_changed=False)
703 source_changed=False, target_changed=False)
704 target_changed = target_ref_id != target_commit.raw_id
704 target_changed = target_ref_id != target_commit.raw_id
705
705
706 if not (source_changed or target_changed):
706 if not (source_changed or target_changed):
707 log.debug("Nothing changed in pull request %s", pull_request)
707 log.debug("Nothing changed in pull request %s", pull_request)
708 return UpdateResponse(
708 return UpdateResponse(
709 executed=False,
709 executed=False,
710 reason=UpdateFailureReason.NO_CHANGE,
710 reason=UpdateFailureReason.NO_CHANGE,
711 old=pull_request, new=None, changes=None,
711 old=pull_request, new=None, changes=None,
712 source_changed=target_changed, target_changed=source_changed)
712 source_changed=target_changed, target_changed=source_changed)
713
713
714 change_in_found = 'target repo' if target_changed else 'source repo'
714 change_in_found = 'target repo' if target_changed else 'source repo'
715 log.debug('Updating pull request because of change in %s detected',
715 log.debug('Updating pull request because of change in %s detected',
716 change_in_found)
716 change_in_found)
717
717
718 # Finally there is a need for an update, in case of source change
718 # Finally there is a need for an update, in case of source change
719 # we create a new version, else just an update
719 # we create a new version, else just an update
720 if source_changed:
720 if source_changed:
721 pull_request_version = self._create_version_from_snapshot(pull_request)
721 pull_request_version = self._create_version_from_snapshot(pull_request)
722 self._link_comments_to_version(pull_request_version)
722 self._link_comments_to_version(pull_request_version)
723 else:
723 else:
724 try:
724 try:
725 ver = pull_request.versions[-1]
725 ver = pull_request.versions[-1]
726 except IndexError:
726 except IndexError:
727 ver = None
727 ver = None
728
728
729 pull_request.pull_request_version_id = \
729 pull_request.pull_request_version_id = \
730 ver.pull_request_version_id if ver else None
730 ver.pull_request_version_id if ver else None
731 pull_request_version = pull_request
731 pull_request_version = pull_request
732
732
733 try:
733 try:
734 if target_ref_type in ('tag', 'branch', 'book'):
734 if target_ref_type in ('tag', 'branch', 'book'):
735 target_commit = target_repo.get_commit(target_ref_name)
735 target_commit = target_repo.get_commit(target_ref_name)
736 else:
736 else:
737 target_commit = target_repo.get_commit(target_ref_id)
737 target_commit = target_repo.get_commit(target_ref_id)
738 except CommitDoesNotExistError:
738 except CommitDoesNotExistError:
739 return UpdateResponse(
739 return UpdateResponse(
740 executed=False,
740 executed=False,
741 reason=UpdateFailureReason.MISSING_TARGET_REF,
741 reason=UpdateFailureReason.MISSING_TARGET_REF,
742 old=pull_request, new=None, changes=None,
742 old=pull_request, new=None, changes=None,
743 source_changed=source_changed, target_changed=target_changed)
743 source_changed=source_changed, target_changed=target_changed)
744
744
745 # re-compute commit ids
745 # re-compute commit ids
746 old_commit_ids = pull_request.revisions
746 old_commit_ids = pull_request.revisions
747 pre_load = ["author", "branch", "date", "message"]
747 pre_load = ["author", "branch", "date", "message"]
748 commit_ranges = target_repo.compare(
748 commit_ranges = target_repo.compare(
749 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
749 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
750 pre_load=pre_load)
750 pre_load=pre_load)
751
751
752 ancestor = target_repo.get_common_ancestor(
752 ancestor = target_repo.get_common_ancestor(
753 target_commit.raw_id, source_commit.raw_id, source_repo)
753 target_commit.raw_id, source_commit.raw_id, source_repo)
754
754
755 pull_request.source_ref = '%s:%s:%s' % (
755 pull_request.source_ref = '%s:%s:%s' % (
756 source_ref_type, source_ref_name, source_commit.raw_id)
756 source_ref_type, source_ref_name, source_commit.raw_id)
757 pull_request.target_ref = '%s:%s:%s' % (
757 pull_request.target_ref = '%s:%s:%s' % (
758 target_ref_type, target_ref_name, ancestor)
758 target_ref_type, target_ref_name, ancestor)
759
759
760 pull_request.revisions = [
760 pull_request.revisions = [
761 commit.raw_id for commit in reversed(commit_ranges)]
761 commit.raw_id for commit in reversed(commit_ranges)]
762 pull_request.updated_on = datetime.datetime.now()
762 pull_request.updated_on = datetime.datetime.now()
763 Session().add(pull_request)
763 Session().add(pull_request)
764 new_commit_ids = pull_request.revisions
764 new_commit_ids = pull_request.revisions
765
765
766 old_diff_data, new_diff_data = self._generate_update_diffs(
766 old_diff_data, new_diff_data = self._generate_update_diffs(
767 pull_request, pull_request_version)
767 pull_request, pull_request_version)
768
768
769 # calculate commit and file changes
769 # calculate commit and file changes
770 changes = self._calculate_commit_id_changes(
770 changes = self._calculate_commit_id_changes(
771 old_commit_ids, new_commit_ids)
771 old_commit_ids, new_commit_ids)
772 file_changes = self._calculate_file_changes(
772 file_changes = self._calculate_file_changes(
773 old_diff_data, new_diff_data)
773 old_diff_data, new_diff_data)
774
774
775 # set comments as outdated if DIFFS changed
775 # set comments as outdated if DIFFS changed
776 CommentsModel().outdate_comments(
776 CommentsModel().outdate_comments(
777 pull_request, old_diff_data=old_diff_data,
777 pull_request, old_diff_data=old_diff_data,
778 new_diff_data=new_diff_data)
778 new_diff_data=new_diff_data)
779
779
780 commit_changes = (changes.added or changes.removed)
780 commit_changes = (changes.added or changes.removed)
781 file_node_changes = (
781 file_node_changes = (
782 file_changes.added or file_changes.modified or file_changes.removed)
782 file_changes.added or file_changes.modified or file_changes.removed)
783 pr_has_changes = commit_changes or file_node_changes
783 pr_has_changes = commit_changes or file_node_changes
784
784
785 # Add an automatic comment to the pull request, in case
785 # Add an automatic comment to the pull request, in case
786 # anything has changed
786 # anything has changed
787 if pr_has_changes:
787 if pr_has_changes:
788 update_comment = CommentsModel().create(
788 update_comment = CommentsModel().create(
789 text=self._render_update_message(changes, file_changes),
789 text=self._render_update_message(changes, file_changes),
790 repo=pull_request.target_repo,
790 repo=pull_request.target_repo,
791 user=pull_request.author,
791 user=pull_request.author,
792 pull_request=pull_request,
792 pull_request=pull_request,
793 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
793 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
794
794
795 # Update status to "Under Review" for added commits
795 # Update status to "Under Review" for added commits
796 for commit_id in changes.added:
796 for commit_id in changes.added:
797 ChangesetStatusModel().set_status(
797 ChangesetStatusModel().set_status(
798 repo=pull_request.source_repo,
798 repo=pull_request.source_repo,
799 status=ChangesetStatus.STATUS_UNDER_REVIEW,
799 status=ChangesetStatus.STATUS_UNDER_REVIEW,
800 comment=update_comment,
800 comment=update_comment,
801 user=pull_request.author,
801 user=pull_request.author,
802 pull_request=pull_request,
802 pull_request=pull_request,
803 revision=commit_id)
803 revision=commit_id)
804
804
805 log.debug(
805 log.debug(
806 'Updated pull request %s, added_ids: %s, common_ids: %s, '
806 'Updated pull request %s, added_ids: %s, common_ids: %s, '
807 'removed_ids: %s', pull_request.pull_request_id,
807 'removed_ids: %s', pull_request.pull_request_id,
808 changes.added, changes.common, changes.removed)
808 changes.added, changes.common, changes.removed)
809 log.debug(
809 log.debug(
810 'Updated pull request with the following file changes: %s',
810 'Updated pull request with the following file changes: %s',
811 file_changes)
811 file_changes)
812
812
813 log.info(
813 log.info(
814 "Updated pull request %s from commit %s to commit %s, "
814 "Updated pull request %s from commit %s to commit %s, "
815 "stored new version %s of this pull request.",
815 "stored new version %s of this pull request.",
816 pull_request.pull_request_id, source_ref_id,
816 pull_request.pull_request_id, source_ref_id,
817 pull_request.source_ref_parts.commit_id,
817 pull_request.source_ref_parts.commit_id,
818 pull_request_version.pull_request_version_id)
818 pull_request_version.pull_request_version_id)
819 Session().commit()
819 Session().commit()
820 self._trigger_pull_request_hook(
820 self._trigger_pull_request_hook(
821 pull_request, pull_request.author, 'update')
821 pull_request, pull_request.author, 'update')
822
822
823 return UpdateResponse(
823 return UpdateResponse(
824 executed=True, reason=UpdateFailureReason.NONE,
824 executed=True, reason=UpdateFailureReason.NONE,
825 old=pull_request, new=pull_request_version, changes=changes,
825 old=pull_request, new=pull_request_version, changes=changes,
826 source_changed=source_changed, target_changed=target_changed)
826 source_changed=source_changed, target_changed=target_changed)
827
827
828 def _create_version_from_snapshot(self, pull_request):
828 def _create_version_from_snapshot(self, pull_request):
829 version = PullRequestVersion()
829 version = PullRequestVersion()
830 version.title = pull_request.title
830 version.title = pull_request.title
831 version.description = pull_request.description
831 version.description = pull_request.description
832 version.status = pull_request.status
832 version.status = pull_request.status
833 version.created_on = datetime.datetime.now()
833 version.created_on = datetime.datetime.now()
834 version.updated_on = pull_request.updated_on
834 version.updated_on = pull_request.updated_on
835 version.user_id = pull_request.user_id
835 version.user_id = pull_request.user_id
836 version.source_repo = pull_request.source_repo
836 version.source_repo = pull_request.source_repo
837 version.source_ref = pull_request.source_ref
837 version.source_ref = pull_request.source_ref
838 version.target_repo = pull_request.target_repo
838 version.target_repo = pull_request.target_repo
839 version.target_ref = pull_request.target_ref
839 version.target_ref = pull_request.target_ref
840
840
841 version._last_merge_source_rev = pull_request._last_merge_source_rev
841 version._last_merge_source_rev = pull_request._last_merge_source_rev
842 version._last_merge_target_rev = pull_request._last_merge_target_rev
842 version._last_merge_target_rev = pull_request._last_merge_target_rev
843 version.last_merge_status = pull_request.last_merge_status
843 version.last_merge_status = pull_request.last_merge_status
844 version.shadow_merge_ref = pull_request.shadow_merge_ref
844 version.shadow_merge_ref = pull_request.shadow_merge_ref
845 version.merge_rev = pull_request.merge_rev
845 version.merge_rev = pull_request.merge_rev
846 version.reviewer_data = pull_request.reviewer_data
846 version.reviewer_data = pull_request.reviewer_data
847
847
848 version.revisions = pull_request.revisions
848 version.revisions = pull_request.revisions
849 version.pull_request = pull_request
849 version.pull_request = pull_request
850 Session().add(version)
850 Session().add(version)
851 Session().flush()
851 Session().flush()
852
852
853 return version
853 return version
854
854
855 def _generate_update_diffs(self, pull_request, pull_request_version):
855 def _generate_update_diffs(self, pull_request, pull_request_version):
856
856
857 diff_context = (
857 diff_context = (
858 self.DIFF_CONTEXT +
858 self.DIFF_CONTEXT +
859 CommentsModel.needed_extra_diff_context())
859 CommentsModel.needed_extra_diff_context())
860
860
861 source_repo = pull_request_version.source_repo
861 source_repo = pull_request_version.source_repo
862 source_ref_id = pull_request_version.source_ref_parts.commit_id
862 source_ref_id = pull_request_version.source_ref_parts.commit_id
863 target_ref_id = pull_request_version.target_ref_parts.commit_id
863 target_ref_id = pull_request_version.target_ref_parts.commit_id
864 old_diff = self._get_diff_from_pr_or_version(
864 old_diff = self._get_diff_from_pr_or_version(
865 source_repo, source_ref_id, target_ref_id, context=diff_context)
865 source_repo, source_ref_id, target_ref_id, context=diff_context)
866
866
867 source_repo = pull_request.source_repo
867 source_repo = pull_request.source_repo
868 source_ref_id = pull_request.source_ref_parts.commit_id
868 source_ref_id = pull_request.source_ref_parts.commit_id
869 target_ref_id = pull_request.target_ref_parts.commit_id
869 target_ref_id = pull_request.target_ref_parts.commit_id
870
870
871 new_diff = self._get_diff_from_pr_or_version(
871 new_diff = self._get_diff_from_pr_or_version(
872 source_repo, source_ref_id, target_ref_id, context=diff_context)
872 source_repo, source_ref_id, target_ref_id, context=diff_context)
873
873
874 old_diff_data = diffs.DiffProcessor(old_diff)
874 old_diff_data = diffs.DiffProcessor(old_diff)
875 old_diff_data.prepare()
875 old_diff_data.prepare()
876 new_diff_data = diffs.DiffProcessor(new_diff)
876 new_diff_data = diffs.DiffProcessor(new_diff)
877 new_diff_data.prepare()
877 new_diff_data.prepare()
878
878
879 return old_diff_data, new_diff_data
879 return old_diff_data, new_diff_data
880
880
881 def _link_comments_to_version(self, pull_request_version):
881 def _link_comments_to_version(self, pull_request_version):
882 """
882 """
883 Link all unlinked comments of this pull request to the given version.
883 Link all unlinked comments of this pull request to the given version.
884
884
885 :param pull_request_version: The `PullRequestVersion` to which
885 :param pull_request_version: The `PullRequestVersion` to which
886 the comments shall be linked.
886 the comments shall be linked.
887
887
888 """
888 """
889 pull_request = pull_request_version.pull_request
889 pull_request = pull_request_version.pull_request
890 comments = ChangesetComment.query()\
890 comments = ChangesetComment.query()\
891 .filter(
891 .filter(
892 # TODO: johbo: Should we query for the repo at all here?
892 # TODO: johbo: Should we query for the repo at all here?
893 # Pending decision on how comments of PRs are to be related
893 # Pending decision on how comments of PRs are to be related
894 # to either the source repo, the target repo or no repo at all.
894 # to either the source repo, the target repo or no repo at all.
895 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
895 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
896 ChangesetComment.pull_request == pull_request,
896 ChangesetComment.pull_request == pull_request,
897 ChangesetComment.pull_request_version == None)\
897 ChangesetComment.pull_request_version == None)\
898 .order_by(ChangesetComment.comment_id.asc())
898 .order_by(ChangesetComment.comment_id.asc())
899
899
900 # TODO: johbo: Find out why this breaks if it is done in a bulk
900 # TODO: johbo: Find out why this breaks if it is done in a bulk
901 # operation.
901 # operation.
902 for comment in comments:
902 for comment in comments:
903 comment.pull_request_version_id = (
903 comment.pull_request_version_id = (
904 pull_request_version.pull_request_version_id)
904 pull_request_version.pull_request_version_id)
905 Session().add(comment)
905 Session().add(comment)
906
906
907 def _calculate_commit_id_changes(self, old_ids, new_ids):
907 def _calculate_commit_id_changes(self, old_ids, new_ids):
908 added = [x for x in new_ids if x not in old_ids]
908 added = [x for x in new_ids if x not in old_ids]
909 common = [x for x in new_ids if x in old_ids]
909 common = [x for x in new_ids if x in old_ids]
910 removed = [x for x in old_ids if x not in new_ids]
910 removed = [x for x in old_ids if x not in new_ids]
911 total = new_ids
911 total = new_ids
912 return ChangeTuple(added, common, removed, total)
912 return ChangeTuple(added, common, removed, total)
913
913
914 def _calculate_file_changes(self, old_diff_data, new_diff_data):
914 def _calculate_file_changes(self, old_diff_data, new_diff_data):
915
915
916 old_files = OrderedDict()
916 old_files = OrderedDict()
917 for diff_data in old_diff_data.parsed_diff:
917 for diff_data in old_diff_data.parsed_diff:
918 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
918 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
919
919
920 added_files = []
920 added_files = []
921 modified_files = []
921 modified_files = []
922 removed_files = []
922 removed_files = []
923 for diff_data in new_diff_data.parsed_diff:
923 for diff_data in new_diff_data.parsed_diff:
924 new_filename = diff_data['filename']
924 new_filename = diff_data['filename']
925 new_hash = md5_safe(diff_data['raw_diff'])
925 new_hash = md5_safe(diff_data['raw_diff'])
926
926
927 old_hash = old_files.get(new_filename)
927 old_hash = old_files.get(new_filename)
928 if not old_hash:
928 if not old_hash:
929 # file is not present in old diff, means it's added
929 # file is not present in old diff, means it's added
930 added_files.append(new_filename)
930 added_files.append(new_filename)
931 else:
931 else:
932 if new_hash != old_hash:
932 if new_hash != old_hash:
933 modified_files.append(new_filename)
933 modified_files.append(new_filename)
934 # now remove a file from old, since we have seen it already
934 # now remove a file from old, since we have seen it already
935 del old_files[new_filename]
935 del old_files[new_filename]
936
936
937 # removed files is when there are present in old, but not in NEW,
937 # removed files is when there are present in old, but not in NEW,
938 # since we remove old files that are present in new diff, left-overs
938 # since we remove old files that are present in new diff, left-overs
939 # if any should be the removed files
939 # if any should be the removed files
940 removed_files.extend(old_files.keys())
940 removed_files.extend(old_files.keys())
941
941
942 return FileChangeTuple(added_files, modified_files, removed_files)
942 return FileChangeTuple(added_files, modified_files, removed_files)
943
943
944 def _render_update_message(self, changes, file_changes):
944 def _render_update_message(self, changes, file_changes):
945 """
945 """
946 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
946 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
947 so it's always looking the same disregarding on which default
947 so it's always looking the same disregarding on which default
948 renderer system is using.
948 renderer system is using.
949
949
950 :param changes: changes named tuple
950 :param changes: changes named tuple
951 :param file_changes: file changes named tuple
951 :param file_changes: file changes named tuple
952
952
953 """
953 """
954 new_status = ChangesetStatus.get_status_lbl(
954 new_status = ChangesetStatus.get_status_lbl(
955 ChangesetStatus.STATUS_UNDER_REVIEW)
955 ChangesetStatus.STATUS_UNDER_REVIEW)
956
956
957 changed_files = (
957 changed_files = (
958 file_changes.added + file_changes.modified + file_changes.removed)
958 file_changes.added + file_changes.modified + file_changes.removed)
959
959
960 params = {
960 params = {
961 'under_review_label': new_status,
961 'under_review_label': new_status,
962 'added_commits': changes.added,
962 'added_commits': changes.added,
963 'removed_commits': changes.removed,
963 'removed_commits': changes.removed,
964 'changed_files': changed_files,
964 'changed_files': changed_files,
965 'added_files': file_changes.added,
965 'added_files': file_changes.added,
966 'modified_files': file_changes.modified,
966 'modified_files': file_changes.modified,
967 'removed_files': file_changes.removed,
967 'removed_files': file_changes.removed,
968 }
968 }
969 renderer = RstTemplateRenderer()
969 renderer = RstTemplateRenderer()
970 return renderer.render('pull_request_update.mako', **params)
970 return renderer.render('pull_request_update.mako', **params)
971
971
972 def edit(self, pull_request, title, description, user):
972 def edit(self, pull_request, title, description, user):
973 pull_request = self.__get_pull_request(pull_request)
973 pull_request = self.__get_pull_request(pull_request)
974 old_data = pull_request.get_api_data(with_merge_state=False)
974 old_data = pull_request.get_api_data(with_merge_state=False)
975 if pull_request.is_closed():
975 if pull_request.is_closed():
976 raise ValueError('This pull request is closed')
976 raise ValueError('This pull request is closed')
977 if title:
977 if title:
978 pull_request.title = title
978 pull_request.title = title
979 pull_request.description = description
979 pull_request.description = description
980 pull_request.updated_on = datetime.datetime.now()
980 pull_request.updated_on = datetime.datetime.now()
981 Session().add(pull_request)
981 Session().add(pull_request)
982 self._log_audit_action(
982 self._log_audit_action(
983 'repo.pull_request.edit', {'old_data': old_data},
983 'repo.pull_request.edit', {'old_data': old_data},
984 user, pull_request)
984 user, pull_request)
985
985
986 def update_reviewers(self, pull_request, reviewer_data, user):
986 def update_reviewers(self, pull_request, reviewer_data, user):
987 """
987 """
988 Update the reviewers in the pull request
988 Update the reviewers in the pull request
989
989
990 :param pull_request: the pr to update
990 :param pull_request: the pr to update
991 :param reviewer_data: list of tuples
991 :param reviewer_data: list of tuples
992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
992 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
993 """
993 """
994 pull_request = self.__get_pull_request(pull_request)
994 pull_request = self.__get_pull_request(pull_request)
995 if pull_request.is_closed():
995 if pull_request.is_closed():
996 raise ValueError('This pull request is closed')
996 raise ValueError('This pull request is closed')
997
997
998 reviewers = {}
998 reviewers = {}
999 for user_id, reasons, mandatory, rules in reviewer_data:
999 for user_id, reasons, mandatory, rules in reviewer_data:
1000 if isinstance(user_id, (int, basestring)):
1000 if isinstance(user_id, (int, basestring)):
1001 user_id = self._get_user(user_id).user_id
1001 user_id = self._get_user(user_id).user_id
1002 reviewers[user_id] = {
1002 reviewers[user_id] = {
1003 'reasons': reasons, 'mandatory': mandatory}
1003 'reasons': reasons, 'mandatory': mandatory}
1004
1004
1005 reviewers_ids = set(reviewers.keys())
1005 reviewers_ids = set(reviewers.keys())
1006 current_reviewers = PullRequestReviewers.query()\
1006 current_reviewers = PullRequestReviewers.query()\
1007 .filter(PullRequestReviewers.pull_request ==
1007 .filter(PullRequestReviewers.pull_request ==
1008 pull_request).all()
1008 pull_request).all()
1009 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1009 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1010
1010
1011 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1011 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1012 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1012 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1013
1013
1014 log.debug("Adding %s reviewers", ids_to_add)
1014 log.debug("Adding %s reviewers", ids_to_add)
1015 log.debug("Removing %s reviewers", ids_to_remove)
1015 log.debug("Removing %s reviewers", ids_to_remove)
1016 changed = False
1016 changed = False
1017 for uid in ids_to_add:
1017 for uid in ids_to_add:
1018 changed = True
1018 changed = True
1019 _usr = self._get_user(uid)
1019 _usr = self._get_user(uid)
1020 reviewer = PullRequestReviewers()
1020 reviewer = PullRequestReviewers()
1021 reviewer.user = _usr
1021 reviewer.user = _usr
1022 reviewer.pull_request = pull_request
1022 reviewer.pull_request = pull_request
1023 reviewer.reasons = reviewers[uid]['reasons']
1023 reviewer.reasons = reviewers[uid]['reasons']
1024 # NOTE(marcink): mandatory shouldn't be changed now
1024 # NOTE(marcink): mandatory shouldn't be changed now
1025 # reviewer.mandatory = reviewers[uid]['reasons']
1025 # reviewer.mandatory = reviewers[uid]['reasons']
1026 Session().add(reviewer)
1026 Session().add(reviewer)
1027 self._log_audit_action(
1027 self._log_audit_action(
1028 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1028 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1029 user, pull_request)
1029 user, pull_request)
1030
1030
1031 for uid in ids_to_remove:
1031 for uid in ids_to_remove:
1032 changed = True
1032 changed = True
1033 reviewers = PullRequestReviewers.query()\
1033 reviewers = PullRequestReviewers.query()\
1034 .filter(PullRequestReviewers.user_id == uid,
1034 .filter(PullRequestReviewers.user_id == uid,
1035 PullRequestReviewers.pull_request == pull_request)\
1035 PullRequestReviewers.pull_request == pull_request)\
1036 .all()
1036 .all()
1037 # use .all() in case we accidentally added the same person twice
1037 # use .all() in case we accidentally added the same person twice
1038 # this CAN happen due to the lack of DB checks
1038 # this CAN happen due to the lack of DB checks
1039 for obj in reviewers:
1039 for obj in reviewers:
1040 old_data = obj.get_dict()
1040 old_data = obj.get_dict()
1041 Session().delete(obj)
1041 Session().delete(obj)
1042 self._log_audit_action(
1042 self._log_audit_action(
1043 'repo.pull_request.reviewer.delete',
1043 'repo.pull_request.reviewer.delete',
1044 {'old_data': old_data}, user, pull_request)
1044 {'old_data': old_data}, user, pull_request)
1045
1045
1046 if changed:
1046 if changed:
1047 pull_request.updated_on = datetime.datetime.now()
1047 pull_request.updated_on = datetime.datetime.now()
1048 Session().add(pull_request)
1048 Session().add(pull_request)
1049
1049
1050 self.notify_reviewers(pull_request, ids_to_add)
1050 self.notify_reviewers(pull_request, ids_to_add)
1051 return ids_to_add, ids_to_remove
1051 return ids_to_add, ids_to_remove
1052
1052
1053 def get_url(self, pull_request, request=None, permalink=False):
1053 def get_url(self, pull_request, request=None, permalink=False):
1054 if not request:
1054 if not request:
1055 request = get_current_request()
1055 request = get_current_request()
1056
1056
1057 if permalink:
1057 if permalink:
1058 return request.route_url(
1058 return request.route_url(
1059 'pull_requests_global',
1059 'pull_requests_global',
1060 pull_request_id=pull_request.pull_request_id,)
1060 pull_request_id=pull_request.pull_request_id,)
1061 else:
1061 else:
1062 return request.route_url('pullrequest_show',
1062 return request.route_url('pullrequest_show',
1063 repo_name=safe_str(pull_request.target_repo.repo_name),
1063 repo_name=safe_str(pull_request.target_repo.repo_name),
1064 pull_request_id=pull_request.pull_request_id,)
1064 pull_request_id=pull_request.pull_request_id,)
1065
1065
1066 def get_shadow_clone_url(self, pull_request):
1066 def get_shadow_clone_url(self, pull_request, request=None):
1067 """
1067 """
1068 Returns qualified url pointing to the shadow repository. If this pull
1068 Returns qualified url pointing to the shadow repository. If this pull
1069 request is closed there is no shadow repository and ``None`` will be
1069 request is closed there is no shadow repository and ``None`` will be
1070 returned.
1070 returned.
1071 """
1071 """
1072 if pull_request.is_closed():
1072 if pull_request.is_closed():
1073 return None
1073 return None
1074 else:
1074 else:
1075 pr_url = urllib.unquote(self.get_url(pull_request))
1075 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1076 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1076 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1077
1077
1078 def notify_reviewers(self, pull_request, reviewers_ids):
1078 def notify_reviewers(self, pull_request, reviewers_ids):
1079 # notification to reviewers
1079 # notification to reviewers
1080 if not reviewers_ids:
1080 if not reviewers_ids:
1081 return
1081 return
1082
1082
1083 pull_request_obj = pull_request
1083 pull_request_obj = pull_request
1084 # get the current participants of this pull request
1084 # get the current participants of this pull request
1085 recipients = reviewers_ids
1085 recipients = reviewers_ids
1086 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1086 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1087
1087
1088 pr_source_repo = pull_request_obj.source_repo
1088 pr_source_repo = pull_request_obj.source_repo
1089 pr_target_repo = pull_request_obj.target_repo
1089 pr_target_repo = pull_request_obj.target_repo
1090
1090
1091 pr_url = h.route_url('pullrequest_show',
1091 pr_url = h.route_url('pullrequest_show',
1092 repo_name=pr_target_repo.repo_name,
1092 repo_name=pr_target_repo.repo_name,
1093 pull_request_id=pull_request_obj.pull_request_id,)
1093 pull_request_id=pull_request_obj.pull_request_id,)
1094
1094
1095 # set some variables for email notification
1095 # set some variables for email notification
1096 pr_target_repo_url = h.route_url(
1096 pr_target_repo_url = h.route_url(
1097 'repo_summary', repo_name=pr_target_repo.repo_name)
1097 'repo_summary', repo_name=pr_target_repo.repo_name)
1098
1098
1099 pr_source_repo_url = h.route_url(
1099 pr_source_repo_url = h.route_url(
1100 'repo_summary', repo_name=pr_source_repo.repo_name)
1100 'repo_summary', repo_name=pr_source_repo.repo_name)
1101
1101
1102 # pull request specifics
1102 # pull request specifics
1103 pull_request_commits = [
1103 pull_request_commits = [
1104 (x.raw_id, x.message)
1104 (x.raw_id, x.message)
1105 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1105 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1106
1106
1107 kwargs = {
1107 kwargs = {
1108 'user': pull_request.author,
1108 'user': pull_request.author,
1109 'pull_request': pull_request_obj,
1109 'pull_request': pull_request_obj,
1110 'pull_request_commits': pull_request_commits,
1110 'pull_request_commits': pull_request_commits,
1111
1111
1112 'pull_request_target_repo': pr_target_repo,
1112 'pull_request_target_repo': pr_target_repo,
1113 'pull_request_target_repo_url': pr_target_repo_url,
1113 'pull_request_target_repo_url': pr_target_repo_url,
1114
1114
1115 'pull_request_source_repo': pr_source_repo,
1115 'pull_request_source_repo': pr_source_repo,
1116 'pull_request_source_repo_url': pr_source_repo_url,
1116 'pull_request_source_repo_url': pr_source_repo_url,
1117
1117
1118 'pull_request_url': pr_url,
1118 'pull_request_url': pr_url,
1119 }
1119 }
1120
1120
1121 # pre-generate the subject for notification itself
1121 # pre-generate the subject for notification itself
1122 (subject,
1122 (subject,
1123 _h, _e, # we don't care about those
1123 _h, _e, # we don't care about those
1124 body_plaintext) = EmailNotificationModel().render_email(
1124 body_plaintext) = EmailNotificationModel().render_email(
1125 notification_type, **kwargs)
1125 notification_type, **kwargs)
1126
1126
1127 # create notification objects, and emails
1127 # create notification objects, and emails
1128 NotificationModel().create(
1128 NotificationModel().create(
1129 created_by=pull_request.author,
1129 created_by=pull_request.author,
1130 notification_subject=subject,
1130 notification_subject=subject,
1131 notification_body=body_plaintext,
1131 notification_body=body_plaintext,
1132 notification_type=notification_type,
1132 notification_type=notification_type,
1133 recipients=recipients,
1133 recipients=recipients,
1134 email_kwargs=kwargs,
1134 email_kwargs=kwargs,
1135 )
1135 )
1136
1136
1137 def delete(self, pull_request, user):
1137 def delete(self, pull_request, user):
1138 pull_request = self.__get_pull_request(pull_request)
1138 pull_request = self.__get_pull_request(pull_request)
1139 old_data = pull_request.get_api_data(with_merge_state=False)
1139 old_data = pull_request.get_api_data(with_merge_state=False)
1140 self._cleanup_merge_workspace(pull_request)
1140 self._cleanup_merge_workspace(pull_request)
1141 self._log_audit_action(
1141 self._log_audit_action(
1142 'repo.pull_request.delete', {'old_data': old_data},
1142 'repo.pull_request.delete', {'old_data': old_data},
1143 user, pull_request)
1143 user, pull_request)
1144 Session().delete(pull_request)
1144 Session().delete(pull_request)
1145
1145
1146 def close_pull_request(self, pull_request, user):
1146 def close_pull_request(self, pull_request, user):
1147 pull_request = self.__get_pull_request(pull_request)
1147 pull_request = self.__get_pull_request(pull_request)
1148 self._cleanup_merge_workspace(pull_request)
1148 self._cleanup_merge_workspace(pull_request)
1149 pull_request.status = PullRequest.STATUS_CLOSED
1149 pull_request.status = PullRequest.STATUS_CLOSED
1150 pull_request.updated_on = datetime.datetime.now()
1150 pull_request.updated_on = datetime.datetime.now()
1151 Session().add(pull_request)
1151 Session().add(pull_request)
1152 self._trigger_pull_request_hook(
1152 self._trigger_pull_request_hook(
1153 pull_request, pull_request.author, 'close')
1153 pull_request, pull_request.author, 'close')
1154
1154
1155 pr_data = pull_request.get_api_data(with_merge_state=False)
1155 pr_data = pull_request.get_api_data(with_merge_state=False)
1156 self._log_audit_action(
1156 self._log_audit_action(
1157 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1157 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1158
1158
1159 def close_pull_request_with_comment(
1159 def close_pull_request_with_comment(
1160 self, pull_request, user, repo, message=None):
1160 self, pull_request, user, repo, message=None):
1161
1161
1162 pull_request_review_status = pull_request.calculated_review_status()
1162 pull_request_review_status = pull_request.calculated_review_status()
1163
1163
1164 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1164 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1165 # approved only if we have voting consent
1165 # approved only if we have voting consent
1166 status = ChangesetStatus.STATUS_APPROVED
1166 status = ChangesetStatus.STATUS_APPROVED
1167 else:
1167 else:
1168 status = ChangesetStatus.STATUS_REJECTED
1168 status = ChangesetStatus.STATUS_REJECTED
1169 status_lbl = ChangesetStatus.get_status_lbl(status)
1169 status_lbl = ChangesetStatus.get_status_lbl(status)
1170
1170
1171 default_message = (
1171 default_message = (
1172 'Closing with status change {transition_icon} {status}.'
1172 'Closing with status change {transition_icon} {status}.'
1173 ).format(transition_icon='>', status=status_lbl)
1173 ).format(transition_icon='>', status=status_lbl)
1174 text = message or default_message
1174 text = message or default_message
1175
1175
1176 # create a comment, and link it to new status
1176 # create a comment, and link it to new status
1177 comment = CommentsModel().create(
1177 comment = CommentsModel().create(
1178 text=text,
1178 text=text,
1179 repo=repo.repo_id,
1179 repo=repo.repo_id,
1180 user=user.user_id,
1180 user=user.user_id,
1181 pull_request=pull_request.pull_request_id,
1181 pull_request=pull_request.pull_request_id,
1182 status_change=status_lbl,
1182 status_change=status_lbl,
1183 status_change_type=status,
1183 status_change_type=status,
1184 closing_pr=True
1184 closing_pr=True
1185 )
1185 )
1186
1186
1187 # calculate old status before we change it
1187 # calculate old status before we change it
1188 old_calculated_status = pull_request.calculated_review_status()
1188 old_calculated_status = pull_request.calculated_review_status()
1189 ChangesetStatusModel().set_status(
1189 ChangesetStatusModel().set_status(
1190 repo.repo_id,
1190 repo.repo_id,
1191 status,
1191 status,
1192 user.user_id,
1192 user.user_id,
1193 comment=comment,
1193 comment=comment,
1194 pull_request=pull_request.pull_request_id
1194 pull_request=pull_request.pull_request_id
1195 )
1195 )
1196
1196
1197 Session().flush()
1197 Session().flush()
1198 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1198 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1199 # we now calculate the status of pull request again, and based on that
1199 # we now calculate the status of pull request again, and based on that
1200 # calculation trigger status change. This might happen in cases
1200 # calculation trigger status change. This might happen in cases
1201 # that non-reviewer admin closes a pr, which means his vote doesn't
1201 # that non-reviewer admin closes a pr, which means his vote doesn't
1202 # change the status, while if he's a reviewer this might change it.
1202 # change the status, while if he's a reviewer this might change it.
1203 calculated_status = pull_request.calculated_review_status()
1203 calculated_status = pull_request.calculated_review_status()
1204 if old_calculated_status != calculated_status:
1204 if old_calculated_status != calculated_status:
1205 self._trigger_pull_request_hook(
1205 self._trigger_pull_request_hook(
1206 pull_request, user, 'review_status_change')
1206 pull_request, user, 'review_status_change')
1207
1207
1208 # finally close the PR
1208 # finally close the PR
1209 PullRequestModel().close_pull_request(
1209 PullRequestModel().close_pull_request(
1210 pull_request.pull_request_id, user)
1210 pull_request.pull_request_id, user)
1211
1211
1212 return comment, status
1212 return comment, status
1213
1213
1214 def merge_status(self, pull_request, translator=None):
1214 def merge_status(self, pull_request, translator=None):
1215 _ = translator or get_current_request().translate
1215 _ = translator or get_current_request().translate
1216
1216
1217 if not self._is_merge_enabled(pull_request):
1217 if not self._is_merge_enabled(pull_request):
1218 return False, _('Server-side pull request merging is disabled.')
1218 return False, _('Server-side pull request merging is disabled.')
1219 if pull_request.is_closed():
1219 if pull_request.is_closed():
1220 return False, _('This pull request is closed.')
1220 return False, _('This pull request is closed.')
1221 merge_possible, msg = self._check_repo_requirements(
1221 merge_possible, msg = self._check_repo_requirements(
1222 target=pull_request.target_repo, source=pull_request.source_repo,
1222 target=pull_request.target_repo, source=pull_request.source_repo,
1223 translator=_)
1223 translator=_)
1224 if not merge_possible:
1224 if not merge_possible:
1225 return merge_possible, msg
1225 return merge_possible, msg
1226
1226
1227 try:
1227 try:
1228 resp = self._try_merge(pull_request)
1228 resp = self._try_merge(pull_request)
1229 log.debug("Merge response: %s", resp)
1229 log.debug("Merge response: %s", resp)
1230 status = resp.possible, self.merge_status_message(
1230 status = resp.possible, self.merge_status_message(
1231 resp.failure_reason)
1231 resp.failure_reason)
1232 except NotImplementedError:
1232 except NotImplementedError:
1233 status = False, _('Pull request merging is not supported.')
1233 status = False, _('Pull request merging is not supported.')
1234
1234
1235 return status
1235 return status
1236
1236
1237 def _check_repo_requirements(self, target, source, translator):
1237 def _check_repo_requirements(self, target, source, translator):
1238 """
1238 """
1239 Check if `target` and `source` have compatible requirements.
1239 Check if `target` and `source` have compatible requirements.
1240
1240
1241 Currently this is just checking for largefiles.
1241 Currently this is just checking for largefiles.
1242 """
1242 """
1243 _ = translator
1243 _ = translator
1244 target_has_largefiles = self._has_largefiles(target)
1244 target_has_largefiles = self._has_largefiles(target)
1245 source_has_largefiles = self._has_largefiles(source)
1245 source_has_largefiles = self._has_largefiles(source)
1246 merge_possible = True
1246 merge_possible = True
1247 message = u''
1247 message = u''
1248
1248
1249 if target_has_largefiles != source_has_largefiles:
1249 if target_has_largefiles != source_has_largefiles:
1250 merge_possible = False
1250 merge_possible = False
1251 if source_has_largefiles:
1251 if source_has_largefiles:
1252 message = _(
1252 message = _(
1253 'Target repository large files support is disabled.')
1253 'Target repository large files support is disabled.')
1254 else:
1254 else:
1255 message = _(
1255 message = _(
1256 'Source repository large files support is disabled.')
1256 'Source repository large files support is disabled.')
1257
1257
1258 return merge_possible, message
1258 return merge_possible, message
1259
1259
1260 def _has_largefiles(self, repo):
1260 def _has_largefiles(self, repo):
1261 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1261 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1262 'extensions', 'largefiles')
1262 'extensions', 'largefiles')
1263 return largefiles_ui and largefiles_ui[0].active
1263 return largefiles_ui and largefiles_ui[0].active
1264
1264
1265 def _try_merge(self, pull_request):
1265 def _try_merge(self, pull_request):
1266 """
1266 """
1267 Try to merge the pull request and return the merge status.
1267 Try to merge the pull request and return the merge status.
1268 """
1268 """
1269 log.debug(
1269 log.debug(
1270 "Trying out if the pull request %s can be merged.",
1270 "Trying out if the pull request %s can be merged.",
1271 pull_request.pull_request_id)
1271 pull_request.pull_request_id)
1272 target_vcs = pull_request.target_repo.scm_instance()
1272 target_vcs = pull_request.target_repo.scm_instance()
1273
1273
1274 # Refresh the target reference.
1274 # Refresh the target reference.
1275 try:
1275 try:
1276 target_ref = self._refresh_reference(
1276 target_ref = self._refresh_reference(
1277 pull_request.target_ref_parts, target_vcs)
1277 pull_request.target_ref_parts, target_vcs)
1278 except CommitDoesNotExistError:
1278 except CommitDoesNotExistError:
1279 merge_state = MergeResponse(
1279 merge_state = MergeResponse(
1280 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1280 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1281 return merge_state
1281 return merge_state
1282
1282
1283 target_locked = pull_request.target_repo.locked
1283 target_locked = pull_request.target_repo.locked
1284 if target_locked and target_locked[0]:
1284 if target_locked and target_locked[0]:
1285 log.debug("The target repository is locked.")
1285 log.debug("The target repository is locked.")
1286 merge_state = MergeResponse(
1286 merge_state = MergeResponse(
1287 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1287 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1288 elif self._needs_merge_state_refresh(pull_request, target_ref):
1288 elif self._needs_merge_state_refresh(pull_request, target_ref):
1289 log.debug("Refreshing the merge status of the repository.")
1289 log.debug("Refreshing the merge status of the repository.")
1290 merge_state = self._refresh_merge_state(
1290 merge_state = self._refresh_merge_state(
1291 pull_request, target_vcs, target_ref)
1291 pull_request, target_vcs, target_ref)
1292 else:
1292 else:
1293 possible = pull_request.\
1293 possible = pull_request.\
1294 last_merge_status == MergeFailureReason.NONE
1294 last_merge_status == MergeFailureReason.NONE
1295 merge_state = MergeResponse(
1295 merge_state = MergeResponse(
1296 possible, False, None, pull_request.last_merge_status)
1296 possible, False, None, pull_request.last_merge_status)
1297
1297
1298 return merge_state
1298 return merge_state
1299
1299
1300 def _refresh_reference(self, reference, vcs_repository):
1300 def _refresh_reference(self, reference, vcs_repository):
1301 if reference.type in ('branch', 'book'):
1301 if reference.type in ('branch', 'book'):
1302 name_or_id = reference.name
1302 name_or_id = reference.name
1303 else:
1303 else:
1304 name_or_id = reference.commit_id
1304 name_or_id = reference.commit_id
1305 refreshed_commit = vcs_repository.get_commit(name_or_id)
1305 refreshed_commit = vcs_repository.get_commit(name_or_id)
1306 refreshed_reference = Reference(
1306 refreshed_reference = Reference(
1307 reference.type, reference.name, refreshed_commit.raw_id)
1307 reference.type, reference.name, refreshed_commit.raw_id)
1308 return refreshed_reference
1308 return refreshed_reference
1309
1309
1310 def _needs_merge_state_refresh(self, pull_request, target_reference):
1310 def _needs_merge_state_refresh(self, pull_request, target_reference):
1311 return not(
1311 return not(
1312 pull_request.revisions and
1312 pull_request.revisions and
1313 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1313 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1314 target_reference.commit_id == pull_request._last_merge_target_rev)
1314 target_reference.commit_id == pull_request._last_merge_target_rev)
1315
1315
1316 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1316 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1317 workspace_id = self._workspace_id(pull_request)
1317 workspace_id = self._workspace_id(pull_request)
1318 source_vcs = pull_request.source_repo.scm_instance()
1318 source_vcs = pull_request.source_repo.scm_instance()
1319 use_rebase = self._use_rebase_for_merging(pull_request)
1319 use_rebase = self._use_rebase_for_merging(pull_request)
1320 close_branch = self._close_branch_before_merging(pull_request)
1320 close_branch = self._close_branch_before_merging(pull_request)
1321 merge_state = target_vcs.merge(
1321 merge_state = target_vcs.merge(
1322 target_reference, source_vcs, pull_request.source_ref_parts,
1322 target_reference, source_vcs, pull_request.source_ref_parts,
1323 workspace_id, dry_run=True, use_rebase=use_rebase,
1323 workspace_id, dry_run=True, use_rebase=use_rebase,
1324 close_branch=close_branch)
1324 close_branch=close_branch)
1325
1325
1326 # Do not store the response if there was an unknown error.
1326 # Do not store the response if there was an unknown error.
1327 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1327 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1328 pull_request._last_merge_source_rev = \
1328 pull_request._last_merge_source_rev = \
1329 pull_request.source_ref_parts.commit_id
1329 pull_request.source_ref_parts.commit_id
1330 pull_request._last_merge_target_rev = target_reference.commit_id
1330 pull_request._last_merge_target_rev = target_reference.commit_id
1331 pull_request.last_merge_status = merge_state.failure_reason
1331 pull_request.last_merge_status = merge_state.failure_reason
1332 pull_request.shadow_merge_ref = merge_state.merge_ref
1332 pull_request.shadow_merge_ref = merge_state.merge_ref
1333 Session().add(pull_request)
1333 Session().add(pull_request)
1334 Session().commit()
1334 Session().commit()
1335
1335
1336 return merge_state
1336 return merge_state
1337
1337
1338 def _workspace_id(self, pull_request):
1338 def _workspace_id(self, pull_request):
1339 workspace_id = 'pr-%s' % pull_request.pull_request_id
1339 workspace_id = 'pr-%s' % pull_request.pull_request_id
1340 return workspace_id
1340 return workspace_id
1341
1341
1342 def merge_status_message(self, status_code):
1342 def merge_status_message(self, status_code):
1343 """
1343 """
1344 Return a human friendly error message for the given merge status code.
1344 Return a human friendly error message for the given merge status code.
1345 """
1345 """
1346 return self.MERGE_STATUS_MESSAGES[status_code]
1346 return self.MERGE_STATUS_MESSAGES[status_code]
1347
1347
1348 def generate_repo_data(self, repo, commit_id=None, branch=None,
1348 def generate_repo_data(self, repo, commit_id=None, branch=None,
1349 bookmark=None, translator=None):
1349 bookmark=None, translator=None):
1350 from rhodecode.model.repo import RepoModel
1350 from rhodecode.model.repo import RepoModel
1351
1351
1352 all_refs, selected_ref = \
1352 all_refs, selected_ref = \
1353 self._get_repo_pullrequest_sources(
1353 self._get_repo_pullrequest_sources(
1354 repo.scm_instance(), commit_id=commit_id,
1354 repo.scm_instance(), commit_id=commit_id,
1355 branch=branch, bookmark=bookmark, translator=translator)
1355 branch=branch, bookmark=bookmark, translator=translator)
1356
1356
1357 refs_select2 = []
1357 refs_select2 = []
1358 for element in all_refs:
1358 for element in all_refs:
1359 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1359 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1360 refs_select2.append({'text': element[1], 'children': children})
1360 refs_select2.append({'text': element[1], 'children': children})
1361
1361
1362 return {
1362 return {
1363 'user': {
1363 'user': {
1364 'user_id': repo.user.user_id,
1364 'user_id': repo.user.user_id,
1365 'username': repo.user.username,
1365 'username': repo.user.username,
1366 'firstname': repo.user.first_name,
1366 'firstname': repo.user.first_name,
1367 'lastname': repo.user.last_name,
1367 'lastname': repo.user.last_name,
1368 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1368 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1369 },
1369 },
1370 'name': repo.repo_name,
1370 'name': repo.repo_name,
1371 'link': RepoModel().get_url(repo),
1371 'link': RepoModel().get_url(repo),
1372 'description': h.chop_at_smart(repo.description_safe, '\n'),
1372 'description': h.chop_at_smart(repo.description_safe, '\n'),
1373 'refs': {
1373 'refs': {
1374 'all_refs': all_refs,
1374 'all_refs': all_refs,
1375 'selected_ref': selected_ref,
1375 'selected_ref': selected_ref,
1376 'select2_refs': refs_select2
1376 'select2_refs': refs_select2
1377 }
1377 }
1378 }
1378 }
1379
1379
1380 def generate_pullrequest_title(self, source, source_ref, target):
1380 def generate_pullrequest_title(self, source, source_ref, target):
1381 return u'{source}#{at_ref} to {target}'.format(
1381 return u'{source}#{at_ref} to {target}'.format(
1382 source=source,
1382 source=source,
1383 at_ref=source_ref,
1383 at_ref=source_ref,
1384 target=target,
1384 target=target,
1385 )
1385 )
1386
1386
1387 def _cleanup_merge_workspace(self, pull_request):
1387 def _cleanup_merge_workspace(self, pull_request):
1388 # Merging related cleanup
1388 # Merging related cleanup
1389 target_scm = pull_request.target_repo.scm_instance()
1389 target_scm = pull_request.target_repo.scm_instance()
1390 workspace_id = 'pr-%s' % pull_request.pull_request_id
1390 workspace_id = 'pr-%s' % pull_request.pull_request_id
1391
1391
1392 try:
1392 try:
1393 target_scm.cleanup_merge_workspace(workspace_id)
1393 target_scm.cleanup_merge_workspace(workspace_id)
1394 except NotImplementedError:
1394 except NotImplementedError:
1395 pass
1395 pass
1396
1396
1397 def _get_repo_pullrequest_sources(
1397 def _get_repo_pullrequest_sources(
1398 self, repo, commit_id=None, branch=None, bookmark=None,
1398 self, repo, commit_id=None, branch=None, bookmark=None,
1399 translator=None):
1399 translator=None):
1400 """
1400 """
1401 Return a structure with repo's interesting commits, suitable for
1401 Return a structure with repo's interesting commits, suitable for
1402 the selectors in pullrequest controller
1402 the selectors in pullrequest controller
1403
1403
1404 :param commit_id: a commit that must be in the list somehow
1404 :param commit_id: a commit that must be in the list somehow
1405 and selected by default
1405 and selected by default
1406 :param branch: a branch that must be in the list and selected
1406 :param branch: a branch that must be in the list and selected
1407 by default - even if closed
1407 by default - even if closed
1408 :param bookmark: a bookmark that must be in the list and selected
1408 :param bookmark: a bookmark that must be in the list and selected
1409 """
1409 """
1410 _ = translator or get_current_request().translate
1410 _ = translator or get_current_request().translate
1411
1411
1412 commit_id = safe_str(commit_id) if commit_id else None
1412 commit_id = safe_str(commit_id) if commit_id else None
1413 branch = safe_str(branch) if branch else None
1413 branch = safe_str(branch) if branch else None
1414 bookmark = safe_str(bookmark) if bookmark else None
1414 bookmark = safe_str(bookmark) if bookmark else None
1415
1415
1416 selected = None
1416 selected = None
1417
1417
1418 # order matters: first source that has commit_id in it will be selected
1418 # order matters: first source that has commit_id in it will be selected
1419 sources = []
1419 sources = []
1420 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1420 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1421 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1421 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1422
1422
1423 if commit_id:
1423 if commit_id:
1424 ref_commit = (h.short_id(commit_id), commit_id)
1424 ref_commit = (h.short_id(commit_id), commit_id)
1425 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1425 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1426
1426
1427 sources.append(
1427 sources.append(
1428 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1428 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1429 )
1429 )
1430
1430
1431 groups = []
1431 groups = []
1432 for group_key, ref_list, group_name, match in sources:
1432 for group_key, ref_list, group_name, match in sources:
1433 group_refs = []
1433 group_refs = []
1434 for ref_name, ref_id in ref_list:
1434 for ref_name, ref_id in ref_list:
1435 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1435 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1436 group_refs.append((ref_key, ref_name))
1436 group_refs.append((ref_key, ref_name))
1437
1437
1438 if not selected:
1438 if not selected:
1439 if set([commit_id, match]) & set([ref_id, ref_name]):
1439 if set([commit_id, match]) & set([ref_id, ref_name]):
1440 selected = ref_key
1440 selected = ref_key
1441
1441
1442 if group_refs:
1442 if group_refs:
1443 groups.append((group_refs, group_name))
1443 groups.append((group_refs, group_name))
1444
1444
1445 if not selected:
1445 if not selected:
1446 ref = commit_id or branch or bookmark
1446 ref = commit_id or branch or bookmark
1447 if ref:
1447 if ref:
1448 raise CommitDoesNotExistError(
1448 raise CommitDoesNotExistError(
1449 'No commit refs could be found matching: %s' % ref)
1449 'No commit refs could be found matching: %s' % ref)
1450 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1450 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1451 selected = 'branch:%s:%s' % (
1451 selected = 'branch:%s:%s' % (
1452 repo.DEFAULT_BRANCH_NAME,
1452 repo.DEFAULT_BRANCH_NAME,
1453 repo.branches[repo.DEFAULT_BRANCH_NAME]
1453 repo.branches[repo.DEFAULT_BRANCH_NAME]
1454 )
1454 )
1455 elif repo.commit_ids:
1455 elif repo.commit_ids:
1456 # make the user select in this case
1456 # make the user select in this case
1457 selected = None
1457 selected = None
1458 else:
1458 else:
1459 raise EmptyRepositoryError()
1459 raise EmptyRepositoryError()
1460 return groups, selected
1460 return groups, selected
1461
1461
1462 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1462 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1463 return self._get_diff_from_pr_or_version(
1463 return self._get_diff_from_pr_or_version(
1464 source_repo, source_ref_id, target_ref_id, context=context)
1464 source_repo, source_ref_id, target_ref_id, context=context)
1465
1465
1466 def _get_diff_from_pr_or_version(
1466 def _get_diff_from_pr_or_version(
1467 self, source_repo, source_ref_id, target_ref_id, context):
1467 self, source_repo, source_ref_id, target_ref_id, context):
1468 target_commit = source_repo.get_commit(
1468 target_commit = source_repo.get_commit(
1469 commit_id=safe_str(target_ref_id))
1469 commit_id=safe_str(target_ref_id))
1470 source_commit = source_repo.get_commit(
1470 source_commit = source_repo.get_commit(
1471 commit_id=safe_str(source_ref_id))
1471 commit_id=safe_str(source_ref_id))
1472 if isinstance(source_repo, Repository):
1472 if isinstance(source_repo, Repository):
1473 vcs_repo = source_repo.scm_instance()
1473 vcs_repo = source_repo.scm_instance()
1474 else:
1474 else:
1475 vcs_repo = source_repo
1475 vcs_repo = source_repo
1476
1476
1477 # TODO: johbo: In the context of an update, we cannot reach
1477 # TODO: johbo: In the context of an update, we cannot reach
1478 # the old commit anymore with our normal mechanisms. It needs
1478 # the old commit anymore with our normal mechanisms. It needs
1479 # some sort of special support in the vcs layer to avoid this
1479 # some sort of special support in the vcs layer to avoid this
1480 # workaround.
1480 # workaround.
1481 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1481 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1482 vcs_repo.alias == 'git'):
1482 vcs_repo.alias == 'git'):
1483 source_commit.raw_id = safe_str(source_ref_id)
1483 source_commit.raw_id = safe_str(source_ref_id)
1484
1484
1485 log.debug('calculating diff between '
1485 log.debug('calculating diff between '
1486 'source_ref:%s and target_ref:%s for repo `%s`',
1486 'source_ref:%s and target_ref:%s for repo `%s`',
1487 target_ref_id, source_ref_id,
1487 target_ref_id, source_ref_id,
1488 safe_unicode(vcs_repo.path))
1488 safe_unicode(vcs_repo.path))
1489
1489
1490 vcs_diff = vcs_repo.get_diff(
1490 vcs_diff = vcs_repo.get_diff(
1491 commit1=target_commit, commit2=source_commit, context=context)
1491 commit1=target_commit, commit2=source_commit, context=context)
1492 return vcs_diff
1492 return vcs_diff
1493
1493
1494 def _is_merge_enabled(self, pull_request):
1494 def _is_merge_enabled(self, pull_request):
1495 return self._get_general_setting(
1495 return self._get_general_setting(
1496 pull_request, 'rhodecode_pr_merge_enabled')
1496 pull_request, 'rhodecode_pr_merge_enabled')
1497
1497
1498 def _use_rebase_for_merging(self, pull_request):
1498 def _use_rebase_for_merging(self, pull_request):
1499 repo_type = pull_request.target_repo.repo_type
1499 repo_type = pull_request.target_repo.repo_type
1500 if repo_type == 'hg':
1500 if repo_type == 'hg':
1501 return self._get_general_setting(
1501 return self._get_general_setting(
1502 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1502 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1503 elif repo_type == 'git':
1503 elif repo_type == 'git':
1504 return self._get_general_setting(
1504 return self._get_general_setting(
1505 pull_request, 'rhodecode_git_use_rebase_for_merging')
1505 pull_request, 'rhodecode_git_use_rebase_for_merging')
1506
1506
1507 return False
1507 return False
1508
1508
1509 def _close_branch_before_merging(self, pull_request):
1509 def _close_branch_before_merging(self, pull_request):
1510 repo_type = pull_request.target_repo.repo_type
1510 repo_type = pull_request.target_repo.repo_type
1511 if repo_type == 'hg':
1511 if repo_type == 'hg':
1512 return self._get_general_setting(
1512 return self._get_general_setting(
1513 pull_request, 'rhodecode_hg_close_branch_before_merging')
1513 pull_request, 'rhodecode_hg_close_branch_before_merging')
1514 elif repo_type == 'git':
1514 elif repo_type == 'git':
1515 return self._get_general_setting(
1515 return self._get_general_setting(
1516 pull_request, 'rhodecode_git_close_branch_before_merging')
1516 pull_request, 'rhodecode_git_close_branch_before_merging')
1517
1517
1518 return False
1518 return False
1519
1519
1520 def _get_general_setting(self, pull_request, settings_key, default=False):
1520 def _get_general_setting(self, pull_request, settings_key, default=False):
1521 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1521 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1522 settings = settings_model.get_general_settings()
1522 settings = settings_model.get_general_settings()
1523 return settings.get(settings_key, default)
1523 return settings.get(settings_key, default)
1524
1524
1525 def _log_audit_action(self, action, action_data, user, pull_request):
1525 def _log_audit_action(self, action, action_data, user, pull_request):
1526 audit_logger.store(
1526 audit_logger.store(
1527 action=action,
1527 action=action,
1528 action_data=action_data,
1528 action_data=action_data,
1529 user=user,
1529 user=user,
1530 repo=pull_request.target_repo)
1530 repo=pull_request.target_repo)
1531
1531
1532 def get_reviewer_functions(self):
1532 def get_reviewer_functions(self):
1533 """
1533 """
1534 Fetches functions for validation and fetching default reviewers.
1534 Fetches functions for validation and fetching default reviewers.
1535 If available we use the EE package, else we fallback to CE
1535 If available we use the EE package, else we fallback to CE
1536 package functions
1536 package functions
1537 """
1537 """
1538 try:
1538 try:
1539 from rc_reviewers.utils import get_default_reviewers_data
1539 from rc_reviewers.utils import get_default_reviewers_data
1540 from rc_reviewers.utils import validate_default_reviewers
1540 from rc_reviewers.utils import validate_default_reviewers
1541 except ImportError:
1541 except ImportError:
1542 from rhodecode.apps.repository.utils import \
1542 from rhodecode.apps.repository.utils import \
1543 get_default_reviewers_data
1543 get_default_reviewers_data
1544 from rhodecode.apps.repository.utils import \
1544 from rhodecode.apps.repository.utils import \
1545 validate_default_reviewers
1545 validate_default_reviewers
1546
1546
1547 return get_default_reviewers_data, validate_default_reviewers
1547 return get_default_reviewers_data, validate_default_reviewers
1548
1548
1549
1549
1550 class MergeCheck(object):
1550 class MergeCheck(object):
1551 """
1551 """
1552 Perform Merge Checks and returns a check object which stores information
1552 Perform Merge Checks and returns a check object which stores information
1553 about merge errors, and merge conditions
1553 about merge errors, and merge conditions
1554 """
1554 """
1555 TODO_CHECK = 'todo'
1555 TODO_CHECK = 'todo'
1556 PERM_CHECK = 'perm'
1556 PERM_CHECK = 'perm'
1557 REVIEW_CHECK = 'review'
1557 REVIEW_CHECK = 'review'
1558 MERGE_CHECK = 'merge'
1558 MERGE_CHECK = 'merge'
1559
1559
1560 def __init__(self):
1560 def __init__(self):
1561 self.review_status = None
1561 self.review_status = None
1562 self.merge_possible = None
1562 self.merge_possible = None
1563 self.merge_msg = ''
1563 self.merge_msg = ''
1564 self.failed = None
1564 self.failed = None
1565 self.errors = []
1565 self.errors = []
1566 self.error_details = OrderedDict()
1566 self.error_details = OrderedDict()
1567
1567
1568 def push_error(self, error_type, message, error_key, details):
1568 def push_error(self, error_type, message, error_key, details):
1569 self.failed = True
1569 self.failed = True
1570 self.errors.append([error_type, message])
1570 self.errors.append([error_type, message])
1571 self.error_details[error_key] = dict(
1571 self.error_details[error_key] = dict(
1572 details=details,
1572 details=details,
1573 error_type=error_type,
1573 error_type=error_type,
1574 message=message
1574 message=message
1575 )
1575 )
1576
1576
1577 @classmethod
1577 @classmethod
1578 def validate(cls, pull_request, user, translator, fail_early=False):
1578 def validate(cls, pull_request, user, translator, fail_early=False):
1579 _ = translator
1579 _ = translator
1580 merge_check = cls()
1580 merge_check = cls()
1581
1581
1582 # permissions to merge
1582 # permissions to merge
1583 user_allowed_to_merge = PullRequestModel().check_user_merge(
1583 user_allowed_to_merge = PullRequestModel().check_user_merge(
1584 pull_request, user)
1584 pull_request, user)
1585 if not user_allowed_to_merge:
1585 if not user_allowed_to_merge:
1586 log.debug("MergeCheck: cannot merge, approval is pending.")
1586 log.debug("MergeCheck: cannot merge, approval is pending.")
1587
1587
1588 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1588 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1589 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1589 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1590 if fail_early:
1590 if fail_early:
1591 return merge_check
1591 return merge_check
1592
1592
1593 # review status, must be always present
1593 # review status, must be always present
1594 review_status = pull_request.calculated_review_status()
1594 review_status = pull_request.calculated_review_status()
1595 merge_check.review_status = review_status
1595 merge_check.review_status = review_status
1596
1596
1597 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1597 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1598 if not status_approved:
1598 if not status_approved:
1599 log.debug("MergeCheck: cannot merge, approval is pending.")
1599 log.debug("MergeCheck: cannot merge, approval is pending.")
1600
1600
1601 msg = _('Pull request reviewer approval is pending.')
1601 msg = _('Pull request reviewer approval is pending.')
1602
1602
1603 merge_check.push_error(
1603 merge_check.push_error(
1604 'warning', msg, cls.REVIEW_CHECK, review_status)
1604 'warning', msg, cls.REVIEW_CHECK, review_status)
1605
1605
1606 if fail_early:
1606 if fail_early:
1607 return merge_check
1607 return merge_check
1608
1608
1609 # left over TODOs
1609 # left over TODOs
1610 todos = CommentsModel().get_unresolved_todos(pull_request)
1610 todos = CommentsModel().get_unresolved_todos(pull_request)
1611 if todos:
1611 if todos:
1612 log.debug("MergeCheck: cannot merge, {} "
1612 log.debug("MergeCheck: cannot merge, {} "
1613 "unresolved todos left.".format(len(todos)))
1613 "unresolved todos left.".format(len(todos)))
1614
1614
1615 if len(todos) == 1:
1615 if len(todos) == 1:
1616 msg = _('Cannot merge, {} TODO still not resolved.').format(
1616 msg = _('Cannot merge, {} TODO still not resolved.').format(
1617 len(todos))
1617 len(todos))
1618 else:
1618 else:
1619 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1619 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1620 len(todos))
1620 len(todos))
1621
1621
1622 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1622 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1623
1623
1624 if fail_early:
1624 if fail_early:
1625 return merge_check
1625 return merge_check
1626
1626
1627 # merge possible
1627 # merge possible
1628 merge_status, msg = PullRequestModel().merge_status(
1628 merge_status, msg = PullRequestModel().merge_status(
1629 pull_request, translator=translator)
1629 pull_request, translator=translator)
1630 merge_check.merge_possible = merge_status
1630 merge_check.merge_possible = merge_status
1631 merge_check.merge_msg = msg
1631 merge_check.merge_msg = msg
1632 if not merge_status:
1632 if not merge_status:
1633 log.debug(
1633 log.debug(
1634 "MergeCheck: cannot merge, pull request merge not possible.")
1634 "MergeCheck: cannot merge, pull request merge not possible.")
1635 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1635 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1636
1636
1637 if fail_early:
1637 if fail_early:
1638 return merge_check
1638 return merge_check
1639
1639
1640 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1640 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1641 return merge_check
1641 return merge_check
1642
1642
1643 @classmethod
1643 @classmethod
1644 def get_merge_conditions(cls, pull_request, translator):
1644 def get_merge_conditions(cls, pull_request, translator):
1645 _ = translator
1645 _ = translator
1646 merge_details = {}
1646 merge_details = {}
1647
1647
1648 model = PullRequestModel()
1648 model = PullRequestModel()
1649 use_rebase = model._use_rebase_for_merging(pull_request)
1649 use_rebase = model._use_rebase_for_merging(pull_request)
1650
1650
1651 if use_rebase:
1651 if use_rebase:
1652 merge_details['merge_strategy'] = dict(
1652 merge_details['merge_strategy'] = dict(
1653 details={},
1653 details={},
1654 message=_('Merge strategy: rebase')
1654 message=_('Merge strategy: rebase')
1655 )
1655 )
1656 else:
1656 else:
1657 merge_details['merge_strategy'] = dict(
1657 merge_details['merge_strategy'] = dict(
1658 details={},
1658 details={},
1659 message=_('Merge strategy: explicit merge commit')
1659 message=_('Merge strategy: explicit merge commit')
1660 )
1660 )
1661
1661
1662 close_branch = model._close_branch_before_merging(pull_request)
1662 close_branch = model._close_branch_before_merging(pull_request)
1663 if close_branch:
1663 if close_branch:
1664 repo_type = pull_request.target_repo.repo_type
1664 repo_type = pull_request.target_repo.repo_type
1665 if repo_type == 'hg':
1665 if repo_type == 'hg':
1666 close_msg = _('Source branch will be closed after merge.')
1666 close_msg = _('Source branch will be closed after merge.')
1667 elif repo_type == 'git':
1667 elif repo_type == 'git':
1668 close_msg = _('Source branch will be deleted after merge.')
1668 close_msg = _('Source branch will be deleted after merge.')
1669
1669
1670 merge_details['close_branch'] = dict(
1670 merge_details['close_branch'] = dict(
1671 details={},
1671 details={},
1672 message=close_msg
1672 message=close_msg
1673 )
1673 )
1674
1674
1675 return merge_details
1675 return merge_details
1676
1676
1677 ChangeTuple = collections.namedtuple(
1677 ChangeTuple = collections.namedtuple(
1678 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1678 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1679
1679
1680 FileChangeTuple = collections.namedtuple(
1680 FileChangeTuple = collections.namedtuple(
1681 'FileChangeTuple', ['added', 'modified', 'removed'])
1681 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now