##// END OF EJS Templates
pull-requests: forbid doing any changes on closed pull-requests....
marcink -
r2383:6726b773 default
parent child Browse files
Show More
@@ -1,1135 +1,1134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
35 from rhodecode.tests.utils import AssertResponse
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 @pytest.mark.usefixtures('app', 'autologin_user')
63 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
65 class TestPullrequestsView(object):
65 class TestPullrequestsView(object):
66
66
67 def test_index(self, backend):
67 def test_index(self, backend):
68 self.app.get(route_path(
68 self.app.get(route_path(
69 'pullrequest_new',
69 'pullrequest_new',
70 repo_name=backend.repo_name))
70 repo_name=backend.repo_name))
71
71
72 def test_option_menu_create_pull_request_exists(self, backend):
72 def test_option_menu_create_pull_request_exists(self, backend):
73 repo_name = backend.repo_name
73 repo_name = backend.repo_name
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75
75
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
77 'pullrequest_new', repo_name=repo_name)
78 response.mustcontain(create_pr_link)
78 response.mustcontain(create_pr_link)
79
79
80 def test_create_pr_form_with_raw_commit_id(self, backend):
80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 repo = backend.repo
81 repo = backend.repo
82
82
83 self.app.get(
83 self.app.get(
84 route_path('pullrequest_new',
84 route_path('pullrequest_new',
85 repo_name=repo.repo_name,
85 repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 def test_show(self, pr_util, pr_merge_enabled):
90 def test_show(self, pr_util, pr_merge_enabled):
91 pull_request = pr_util.create_pull_request(
91 pull_request = pr_util.create_pull_request(
92 mergeable=pr_merge_enabled, enable_notifications=False)
92 mergeable=pr_merge_enabled, enable_notifications=False)
93
93
94 response = self.app.get(route_path(
94 response = self.app.get(route_path(
95 'pullrequest_show',
95 'pullrequest_show',
96 repo_name=pull_request.target_repo.scm_instance().name,
96 repo_name=pull_request.target_repo.scm_instance().name,
97 pull_request_id=pull_request.pull_request_id))
97 pull_request_id=pull_request.pull_request_id))
98
98
99 for commit_id in pull_request.revisions:
99 for commit_id in pull_request.revisions:
100 response.mustcontain(commit_id)
100 response.mustcontain(commit_id)
101
101
102 assert pull_request.target_ref_parts.type in response
102 assert pull_request.target_ref_parts.type in response
103 assert pull_request.target_ref_parts.name in response
103 assert pull_request.target_ref_parts.name in response
104 target_clone_url = pull_request.target_repo.clone_url()
104 target_clone_url = pull_request.target_repo.clone_url()
105 assert target_clone_url in response
105 assert target_clone_url in response
106
106
107 assert 'class="pull-request-merge"' in response
107 assert 'class="pull-request-merge"' in response
108 assert (
108 assert (
109 'Server-side pull request merging is disabled.'
109 'Server-side pull request merging is disabled.'
110 in response) != pr_merge_enabled
110 in response) != pr_merge_enabled
111
111
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 # Logout
113 # Logout
114 response = self.app.post(
114 response = self.app.post(
115 h.route_path('logout'),
115 h.route_path('logout'),
116 params={'csrf_token': csrf_token})
116 params={'csrf_token': csrf_token})
117 # Login as regular user
117 # Login as regular user
118 response = self.app.post(h.route_path('login'),
118 response = self.app.post(h.route_path('login'),
119 {'username': TEST_USER_REGULAR_LOGIN,
119 {'username': TEST_USER_REGULAR_LOGIN,
120 'password': 'test12'})
120 'password': 'test12'})
121
121
122 pull_request = pr_util.create_pull_request(
122 pull_request = pr_util.create_pull_request(
123 author=TEST_USER_REGULAR_LOGIN)
123 author=TEST_USER_REGULAR_LOGIN)
124
124
125 response = self.app.get(route_path(
125 response = self.app.get(route_path(
126 'pullrequest_show',
126 'pullrequest_show',
127 repo_name=pull_request.target_repo.scm_instance().name,
127 repo_name=pull_request.target_repo.scm_instance().name,
128 pull_request_id=pull_request.pull_request_id))
128 pull_request_id=pull_request.pull_request_id))
129
129
130 response.mustcontain('Server-side pull request merging is disabled.')
130 response.mustcontain('Server-side pull request merging is disabled.')
131
131
132 assert_response = response.assert_response()
132 assert_response = response.assert_response()
133 # for regular user without a merge permissions, we don't see it
133 # for regular user without a merge permissions, we don't see it
134 assert_response.no_element_exists('#close-pull-request-action')
134 assert_response.no_element_exists('#close-pull-request-action')
135
135
136 user_util.grant_user_permission_to_repo(
136 user_util.grant_user_permission_to_repo(
137 pull_request.target_repo,
137 pull_request.target_repo,
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 'repository.write')
139 'repository.write')
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'pullrequest_show',
141 'pullrequest_show',
142 repo_name=pull_request.target_repo.scm_instance().name,
142 repo_name=pull_request.target_repo.scm_instance().name,
143 pull_request_id=pull_request.pull_request_id))
143 pull_request_id=pull_request.pull_request_id))
144
144
145 response.mustcontain('Server-side pull request merging is disabled.')
145 response.mustcontain('Server-side pull request merging is disabled.')
146
146
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 # now regular user has a merge permissions, we have CLOSE button
148 # now regular user has a merge permissions, we have CLOSE button
149 assert_response.one_element_exists('#close-pull-request-action')
149 assert_response.one_element_exists('#close-pull-request-action')
150
150
151 def test_show_invalid_commit_id(self, pr_util):
151 def test_show_invalid_commit_id(self, pr_util):
152 # Simulating invalid revisions which will cause a lookup error
152 # Simulating invalid revisions which will cause a lookup error
153 pull_request = pr_util.create_pull_request()
153 pull_request = pr_util.create_pull_request()
154 pull_request.revisions = ['invalid']
154 pull_request.revisions = ['invalid']
155 Session().add(pull_request)
155 Session().add(pull_request)
156 Session().commit()
156 Session().commit()
157
157
158 response = self.app.get(route_path(
158 response = self.app.get(route_path(
159 'pullrequest_show',
159 'pullrequest_show',
160 repo_name=pull_request.target_repo.scm_instance().name,
160 repo_name=pull_request.target_repo.scm_instance().name,
161 pull_request_id=pull_request.pull_request_id))
161 pull_request_id=pull_request.pull_request_id))
162
162
163 for commit_id in pull_request.revisions:
163 for commit_id in pull_request.revisions:
164 response.mustcontain(commit_id)
164 response.mustcontain(commit_id)
165
165
166 def test_show_invalid_source_reference(self, pr_util):
166 def test_show_invalid_source_reference(self, pr_util):
167 pull_request = pr_util.create_pull_request()
167 pull_request = pr_util.create_pull_request()
168 pull_request.source_ref = 'branch:b:invalid'
168 pull_request.source_ref = 'branch:b:invalid'
169 Session().add(pull_request)
169 Session().add(pull_request)
170 Session().commit()
170 Session().commit()
171
171
172 self.app.get(route_path(
172 self.app.get(route_path(
173 'pullrequest_show',
173 'pullrequest_show',
174 repo_name=pull_request.target_repo.scm_instance().name,
174 repo_name=pull_request.target_repo.scm_instance().name,
175 pull_request_id=pull_request.pull_request_id))
175 pull_request_id=pull_request.pull_request_id))
176
176
177 def test_edit_title_description(self, pr_util, csrf_token):
177 def test_edit_title_description(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180
180
181 response = self.app.post(
181 response = self.app.post(
182 route_path('pullrequest_update',
182 route_path('pullrequest_update',
183 repo_name=pull_request.target_repo.repo_name,
183 repo_name=pull_request.target_repo.repo_name,
184 pull_request_id=pull_request_id),
184 pull_request_id=pull_request_id),
185 params={
185 params={
186 'edit_pull_request': 'true',
186 'edit_pull_request': 'true',
187 'title': 'New title',
187 'title': 'New title',
188 'description': 'New description',
188 'description': 'New description',
189 'csrf_token': csrf_token})
189 'csrf_token': csrf_token})
190
190
191 assert_session_flash(
191 assert_session_flash(
192 response, u'Pull request title & description updated.',
192 response, u'Pull request title & description updated.',
193 category='success')
193 category='success')
194
194
195 pull_request = PullRequest.get(pull_request_id)
195 pull_request = PullRequest.get(pull_request_id)
196 assert pull_request.title == 'New title'
196 assert pull_request.title == 'New title'
197 assert pull_request.description == 'New description'
197 assert pull_request.description == 'New description'
198
198
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request_id = pull_request.pull_request_id
201 pull_request_id = pull_request.pull_request_id
202 repo_name = pull_request.target_repo.repo_name
202 pr_util.close()
203 pr_util.close()
203
204
204 response = self.app.post(
205 response = self.app.post(
205 route_path('pullrequest_update',
206 route_path('pullrequest_update',
206 repo_name=pull_request.target_repo.repo_name,
207 repo_name=repo_name, pull_request_id=pull_request_id),
207 pull_request_id=pull_request_id),
208 params={
208 params={
209 'edit_pull_request': 'true',
209 'edit_pull_request': 'true',
210 'title': 'New title',
210 'title': 'New title',
211 'description': 'New description',
211 'description': 'New description',
212 'csrf_token': csrf_token})
212 'csrf_token': csrf_token}, status=200)
213
214 assert_session_flash(
213 assert_session_flash(
215 response, u'Cannot update closed pull requests.',
214 response, u'Cannot update closed pull requests.',
216 category='error')
215 category='error')
217
216
218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
220
219
221 pull_request = pr_util.create_pull_request()
220 pull_request = pr_util.create_pull_request()
222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
223 Session().add(pull_request)
222 Session().add(pull_request)
224 Session().commit()
223 Session().commit()
225
224
226 pull_request_id = pull_request.pull_request_id
225 pull_request_id = pull_request.pull_request_id
227
226
228 response = self.app.post(
227 response = self.app.post(
229 route_path('pullrequest_update',
228 route_path('pullrequest_update',
230 repo_name=pull_request.target_repo.repo_name,
229 repo_name=pull_request.target_repo.repo_name,
231 pull_request_id=pull_request_id),
230 pull_request_id=pull_request_id),
232 params={'update_commits': 'true',
231 params={'update_commits': 'true',
233 'csrf_token': csrf_token})
232 'csrf_token': csrf_token})
234
233
235 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
236 UpdateFailureReason.MISSING_SOURCE_REF])
235 UpdateFailureReason.MISSING_SOURCE_REF])
237 assert_session_flash(response, expected_msg, category='error')
236 assert_session_flash(response, expected_msg, category='error')
238
237
239 def test_missing_target_reference(self, pr_util, csrf_token):
238 def test_missing_target_reference(self, pr_util, csrf_token):
240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
241 pull_request = pr_util.create_pull_request(
240 pull_request = pr_util.create_pull_request(
242 approved=True, mergeable=True)
241 approved=True, mergeable=True)
243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
244 Session().add(pull_request)
243 Session().add(pull_request)
245 Session().commit()
244 Session().commit()
246
245
247 pull_request_id = pull_request.pull_request_id
246 pull_request_id = pull_request.pull_request_id
248 pull_request_url = route_path(
247 pull_request_url = route_path(
249 'pullrequest_show',
248 'pullrequest_show',
250 repo_name=pull_request.target_repo.repo_name,
249 repo_name=pull_request.target_repo.repo_name,
251 pull_request_id=pull_request_id)
250 pull_request_id=pull_request_id)
252
251
253 response = self.app.get(pull_request_url)
252 response = self.app.get(pull_request_url)
254
253
255 assertr = AssertResponse(response)
254 assertr = AssertResponse(response)
256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
257 MergeFailureReason.MISSING_TARGET_REF]
256 MergeFailureReason.MISSING_TARGET_REF]
258 assertr.element_contains(
257 assertr.element_contains(
259 'span[data-role="merge-message"]', str(expected_msg))
258 'span[data-role="merge-message"]', str(expected_msg))
260
259
261 def test_comment_and_close_pull_request_custom_message_approved(
260 def test_comment_and_close_pull_request_custom_message_approved(
262 self, pr_util, csrf_token, xhr_header):
261 self, pr_util, csrf_token, xhr_header):
263
262
264 pull_request = pr_util.create_pull_request(approved=True)
263 pull_request = pr_util.create_pull_request(approved=True)
265 pull_request_id = pull_request.pull_request_id
264 pull_request_id = pull_request.pull_request_id
266 author = pull_request.user_id
265 author = pull_request.user_id
267 repo = pull_request.target_repo.repo_id
266 repo = pull_request.target_repo.repo_id
268
267
269 self.app.post(
268 self.app.post(
270 route_path('pullrequest_comment_create',
269 route_path('pullrequest_comment_create',
271 repo_name=pull_request.target_repo.scm_instance().name,
270 repo_name=pull_request.target_repo.scm_instance().name,
272 pull_request_id=pull_request_id),
271 pull_request_id=pull_request_id),
273 params={
272 params={
274 'close_pull_request': '1',
273 'close_pull_request': '1',
275 'text': 'Closing a PR',
274 'text': 'Closing a PR',
276 'csrf_token': csrf_token},
275 'csrf_token': csrf_token},
277 extra_environ=xhr_header,)
276 extra_environ=xhr_header,)
278
277
279 journal = UserLog.query()\
278 journal = UserLog.query()\
280 .filter(UserLog.user_id == author)\
279 .filter(UserLog.user_id == author)\
281 .filter(UserLog.repository_id == repo) \
280 .filter(UserLog.repository_id == repo) \
282 .order_by('user_log_id') \
281 .order_by('user_log_id') \
283 .all()
282 .all()
284 assert journal[-1].action == 'repo.pull_request.close'
283 assert journal[-1].action == 'repo.pull_request.close'
285
284
286 pull_request = PullRequest.get(pull_request_id)
285 pull_request = PullRequest.get(pull_request_id)
287 assert pull_request.is_closed()
286 assert pull_request.is_closed()
288
287
289 status = ChangesetStatusModel().get_status(
288 status = ChangesetStatusModel().get_status(
290 pull_request.source_repo, pull_request=pull_request)
289 pull_request.source_repo, pull_request=pull_request)
291 assert status == ChangesetStatus.STATUS_APPROVED
290 assert status == ChangesetStatus.STATUS_APPROVED
292 comments = ChangesetComment().query() \
291 comments = ChangesetComment().query() \
293 .filter(ChangesetComment.pull_request == pull_request) \
292 .filter(ChangesetComment.pull_request == pull_request) \
294 .order_by(ChangesetComment.comment_id.asc())\
293 .order_by(ChangesetComment.comment_id.asc())\
295 .all()
294 .all()
296 assert comments[-1].text == 'Closing a PR'
295 assert comments[-1].text == 'Closing a PR'
297
296
298 def test_comment_force_close_pull_request_rejected(
297 def test_comment_force_close_pull_request_rejected(
299 self, pr_util, csrf_token, xhr_header):
298 self, pr_util, csrf_token, xhr_header):
300 pull_request = pr_util.create_pull_request()
299 pull_request = pr_util.create_pull_request()
301 pull_request_id = pull_request.pull_request_id
300 pull_request_id = pull_request.pull_request_id
302 PullRequestModel().update_reviewers(
301 PullRequestModel().update_reviewers(
303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
302 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
304 pull_request.author)
303 pull_request.author)
305 author = pull_request.user_id
304 author = pull_request.user_id
306 repo = pull_request.target_repo.repo_id
305 repo = pull_request.target_repo.repo_id
307
306
308 self.app.post(
307 self.app.post(
309 route_path('pullrequest_comment_create',
308 route_path('pullrequest_comment_create',
310 repo_name=pull_request.target_repo.scm_instance().name,
309 repo_name=pull_request.target_repo.scm_instance().name,
311 pull_request_id=pull_request_id),
310 pull_request_id=pull_request_id),
312 params={
311 params={
313 'close_pull_request': '1',
312 'close_pull_request': '1',
314 'csrf_token': csrf_token},
313 'csrf_token': csrf_token},
315 extra_environ=xhr_header)
314 extra_environ=xhr_header)
316
315
317 pull_request = PullRequest.get(pull_request_id)
316 pull_request = PullRequest.get(pull_request_id)
318
317
319 journal = UserLog.query()\
318 journal = UserLog.query()\
320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
321 .order_by('user_log_id') \
320 .order_by('user_log_id') \
322 .all()
321 .all()
323 assert journal[-1].action == 'repo.pull_request.close'
322 assert journal[-1].action == 'repo.pull_request.close'
324
323
325 # check only the latest status, not the review status
324 # check only the latest status, not the review status
326 status = ChangesetStatusModel().get_status(
325 status = ChangesetStatusModel().get_status(
327 pull_request.source_repo, pull_request=pull_request)
326 pull_request.source_repo, pull_request=pull_request)
328 assert status == ChangesetStatus.STATUS_REJECTED
327 assert status == ChangesetStatus.STATUS_REJECTED
329
328
330 def test_comment_and_close_pull_request(
329 def test_comment_and_close_pull_request(
331 self, pr_util, csrf_token, xhr_header):
330 self, pr_util, csrf_token, xhr_header):
332 pull_request = pr_util.create_pull_request()
331 pull_request = pr_util.create_pull_request()
333 pull_request_id = pull_request.pull_request_id
332 pull_request_id = pull_request.pull_request_id
334
333
335 response = self.app.post(
334 response = self.app.post(
336 route_path('pullrequest_comment_create',
335 route_path('pullrequest_comment_create',
337 repo_name=pull_request.target_repo.scm_instance().name,
336 repo_name=pull_request.target_repo.scm_instance().name,
338 pull_request_id=pull_request.pull_request_id),
337 pull_request_id=pull_request.pull_request_id),
339 params={
338 params={
340 'close_pull_request': 'true',
339 'close_pull_request': 'true',
341 'csrf_token': csrf_token},
340 'csrf_token': csrf_token},
342 extra_environ=xhr_header)
341 extra_environ=xhr_header)
343
342
344 assert response.json
343 assert response.json
345
344
346 pull_request = PullRequest.get(pull_request_id)
345 pull_request = PullRequest.get(pull_request_id)
347 assert pull_request.is_closed()
346 assert pull_request.is_closed()
348
347
349 # check only the latest status, not the review status
348 # check only the latest status, not the review status
350 status = ChangesetStatusModel().get_status(
349 status = ChangesetStatusModel().get_status(
351 pull_request.source_repo, pull_request=pull_request)
350 pull_request.source_repo, pull_request=pull_request)
352 assert status == ChangesetStatus.STATUS_REJECTED
351 assert status == ChangesetStatus.STATUS_REJECTED
353
352
354 def test_create_pull_request(self, backend, csrf_token):
353 def test_create_pull_request(self, backend, csrf_token):
355 commits = [
354 commits = [
356 {'message': 'ancestor'},
355 {'message': 'ancestor'},
357 {'message': 'change'},
356 {'message': 'change'},
358 {'message': 'change2'},
357 {'message': 'change2'},
359 ]
358 ]
360 commit_ids = backend.create_master_repo(commits)
359 commit_ids = backend.create_master_repo(commits)
361 target = backend.create_repo(heads=['ancestor'])
360 target = backend.create_repo(heads=['ancestor'])
362 source = backend.create_repo(heads=['change2'])
361 source = backend.create_repo(heads=['change2'])
363
362
364 response = self.app.post(
363 response = self.app.post(
365 route_path('pullrequest_create', repo_name=source.repo_name),
364 route_path('pullrequest_create', repo_name=source.repo_name),
366 [
365 [
367 ('source_repo', source.repo_name),
366 ('source_repo', source.repo_name),
368 ('source_ref', 'branch:default:' + commit_ids['change2']),
367 ('source_ref', 'branch:default:' + commit_ids['change2']),
369 ('target_repo', target.repo_name),
368 ('target_repo', target.repo_name),
370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
371 ('common_ancestor', commit_ids['ancestor']),
370 ('common_ancestor', commit_ids['ancestor']),
372 ('pullrequest_desc', 'Description'),
371 ('pullrequest_desc', 'Description'),
373 ('pullrequest_title', 'Title'),
372 ('pullrequest_title', 'Title'),
374 ('__start__', 'review_members:sequence'),
373 ('__start__', 'review_members:sequence'),
375 ('__start__', 'reviewer:mapping'),
374 ('__start__', 'reviewer:mapping'),
376 ('user_id', '1'),
375 ('user_id', '1'),
377 ('__start__', 'reasons:sequence'),
376 ('__start__', 'reasons:sequence'),
378 ('reason', 'Some reason'),
377 ('reason', 'Some reason'),
379 ('__end__', 'reasons:sequence'),
378 ('__end__', 'reasons:sequence'),
380 ('mandatory', 'False'),
379 ('mandatory', 'False'),
381 ('__end__', 'reviewer:mapping'),
380 ('__end__', 'reviewer:mapping'),
382 ('__end__', 'review_members:sequence'),
381 ('__end__', 'review_members:sequence'),
383 ('__start__', 'revisions:sequence'),
382 ('__start__', 'revisions:sequence'),
384 ('revisions', commit_ids['change']),
383 ('revisions', commit_ids['change']),
385 ('revisions', commit_ids['change2']),
384 ('revisions', commit_ids['change2']),
386 ('__end__', 'revisions:sequence'),
385 ('__end__', 'revisions:sequence'),
387 ('user', ''),
386 ('user', ''),
388 ('csrf_token', csrf_token),
387 ('csrf_token', csrf_token),
389 ],
388 ],
390 status=302)
389 status=302)
391
390
392 location = response.headers['Location']
391 location = response.headers['Location']
393 pull_request_id = location.rsplit('/', 1)[1]
392 pull_request_id = location.rsplit('/', 1)[1]
394 assert pull_request_id != 'new'
393 assert pull_request_id != 'new'
395 pull_request = PullRequest.get(int(pull_request_id))
394 pull_request = PullRequest.get(int(pull_request_id))
396
395
397 # check that we have now both revisions
396 # check that we have now both revisions
398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
397 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
398 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
399 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 assert pull_request.target_ref == expected_target_ref
400 assert pull_request.target_ref == expected_target_ref
402
401
403 def test_reviewer_notifications(self, backend, csrf_token):
402 def test_reviewer_notifications(self, backend, csrf_token):
404 # We have to use the app.post for this test so it will create the
403 # We have to use the app.post for this test so it will create the
405 # notifications properly with the new PR
404 # notifications properly with the new PR
406 commits = [
405 commits = [
407 {'message': 'ancestor',
406 {'message': 'ancestor',
408 'added': [FileNode('file_A', content='content_of_ancestor')]},
407 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 {'message': 'change',
408 {'message': 'change',
410 'added': [FileNode('file_a', content='content_of_change')]},
409 'added': [FileNode('file_a', content='content_of_change')]},
411 {'message': 'change-child'},
410 {'message': 'change-child'},
412 {'message': 'ancestor-child', 'parents': ['ancestor'],
411 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 'added': [
412 'added': [
414 FileNode('file_B', content='content_of_ancestor_child')]},
413 FileNode('file_B', content='content_of_ancestor_child')]},
415 {'message': 'ancestor-child-2'},
414 {'message': 'ancestor-child-2'},
416 ]
415 ]
417 commit_ids = backend.create_master_repo(commits)
416 commit_ids = backend.create_master_repo(commits)
418 target = backend.create_repo(heads=['ancestor-child'])
417 target = backend.create_repo(heads=['ancestor-child'])
419 source = backend.create_repo(heads=['change'])
418 source = backend.create_repo(heads=['change'])
420
419
421 response = self.app.post(
420 response = self.app.post(
422 route_path('pullrequest_create', repo_name=source.repo_name),
421 route_path('pullrequest_create', repo_name=source.repo_name),
423 [
422 [
424 ('source_repo', source.repo_name),
423 ('source_repo', source.repo_name),
425 ('source_ref', 'branch:default:' + commit_ids['change']),
424 ('source_ref', 'branch:default:' + commit_ids['change']),
426 ('target_repo', target.repo_name),
425 ('target_repo', target.repo_name),
427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
426 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 ('common_ancestor', commit_ids['ancestor']),
427 ('common_ancestor', commit_ids['ancestor']),
429 ('pullrequest_desc', 'Description'),
428 ('pullrequest_desc', 'Description'),
430 ('pullrequest_title', 'Title'),
429 ('pullrequest_title', 'Title'),
431 ('__start__', 'review_members:sequence'),
430 ('__start__', 'review_members:sequence'),
432 ('__start__', 'reviewer:mapping'),
431 ('__start__', 'reviewer:mapping'),
433 ('user_id', '2'),
432 ('user_id', '2'),
434 ('__start__', 'reasons:sequence'),
433 ('__start__', 'reasons:sequence'),
435 ('reason', 'Some reason'),
434 ('reason', 'Some reason'),
436 ('__end__', 'reasons:sequence'),
435 ('__end__', 'reasons:sequence'),
437 ('mandatory', 'False'),
436 ('mandatory', 'False'),
438 ('__end__', 'reviewer:mapping'),
437 ('__end__', 'reviewer:mapping'),
439 ('__end__', 'review_members:sequence'),
438 ('__end__', 'review_members:sequence'),
440 ('__start__', 'revisions:sequence'),
439 ('__start__', 'revisions:sequence'),
441 ('revisions', commit_ids['change']),
440 ('revisions', commit_ids['change']),
442 ('__end__', 'revisions:sequence'),
441 ('__end__', 'revisions:sequence'),
443 ('user', ''),
442 ('user', ''),
444 ('csrf_token', csrf_token),
443 ('csrf_token', csrf_token),
445 ],
444 ],
446 status=302)
445 status=302)
447
446
448 location = response.headers['Location']
447 location = response.headers['Location']
449
448
450 pull_request_id = location.rsplit('/', 1)[1]
449 pull_request_id = location.rsplit('/', 1)[1]
451 assert pull_request_id != 'new'
450 assert pull_request_id != 'new'
452 pull_request = PullRequest.get(int(pull_request_id))
451 pull_request = PullRequest.get(int(pull_request_id))
453
452
454 # Check that a notification was made
453 # Check that a notification was made
455 notifications = Notification.query()\
454 notifications = Notification.query()\
456 .filter(Notification.created_by == pull_request.author.user_id,
455 .filter(Notification.created_by == pull_request.author.user_id,
457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
456 Notification.type_ == Notification.TYPE_PULL_REQUEST,
458 Notification.subject.contains(
457 Notification.subject.contains(
459 "wants you to review pull request #%s" % pull_request_id))
458 "wants you to review pull request #%s" % pull_request_id))
460 assert len(notifications.all()) == 1
459 assert len(notifications.all()) == 1
461
460
462 # Change reviewers and check that a notification was made
461 # Change reviewers and check that a notification was made
463 PullRequestModel().update_reviewers(
462 PullRequestModel().update_reviewers(
464 pull_request.pull_request_id, [(1, [], False)],
463 pull_request.pull_request_id, [(1, [], False)],
465 pull_request.author)
464 pull_request.author)
466 assert len(notifications.all()) == 2
465 assert len(notifications.all()) == 2
467
466
468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
467 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
469 csrf_token):
468 csrf_token):
470 commits = [
469 commits = [
471 {'message': 'ancestor',
470 {'message': 'ancestor',
472 'added': [FileNode('file_A', content='content_of_ancestor')]},
471 'added': [FileNode('file_A', content='content_of_ancestor')]},
473 {'message': 'change',
472 {'message': 'change',
474 'added': [FileNode('file_a', content='content_of_change')]},
473 'added': [FileNode('file_a', content='content_of_change')]},
475 {'message': 'change-child'},
474 {'message': 'change-child'},
476 {'message': 'ancestor-child', 'parents': ['ancestor'],
475 {'message': 'ancestor-child', 'parents': ['ancestor'],
477 'added': [
476 'added': [
478 FileNode('file_B', content='content_of_ancestor_child')]},
477 FileNode('file_B', content='content_of_ancestor_child')]},
479 {'message': 'ancestor-child-2'},
478 {'message': 'ancestor-child-2'},
480 ]
479 ]
481 commit_ids = backend.create_master_repo(commits)
480 commit_ids = backend.create_master_repo(commits)
482 target = backend.create_repo(heads=['ancestor-child'])
481 target = backend.create_repo(heads=['ancestor-child'])
483 source = backend.create_repo(heads=['change'])
482 source = backend.create_repo(heads=['change'])
484
483
485 response = self.app.post(
484 response = self.app.post(
486 route_path('pullrequest_create', repo_name=source.repo_name),
485 route_path('pullrequest_create', repo_name=source.repo_name),
487 [
486 [
488 ('source_repo', source.repo_name),
487 ('source_repo', source.repo_name),
489 ('source_ref', 'branch:default:' + commit_ids['change']),
488 ('source_ref', 'branch:default:' + commit_ids['change']),
490 ('target_repo', target.repo_name),
489 ('target_repo', target.repo_name),
491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
490 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
492 ('common_ancestor', commit_ids['ancestor']),
491 ('common_ancestor', commit_ids['ancestor']),
493 ('pullrequest_desc', 'Description'),
492 ('pullrequest_desc', 'Description'),
494 ('pullrequest_title', 'Title'),
493 ('pullrequest_title', 'Title'),
495 ('__start__', 'review_members:sequence'),
494 ('__start__', 'review_members:sequence'),
496 ('__start__', 'reviewer:mapping'),
495 ('__start__', 'reviewer:mapping'),
497 ('user_id', '1'),
496 ('user_id', '1'),
498 ('__start__', 'reasons:sequence'),
497 ('__start__', 'reasons:sequence'),
499 ('reason', 'Some reason'),
498 ('reason', 'Some reason'),
500 ('__end__', 'reasons:sequence'),
499 ('__end__', 'reasons:sequence'),
501 ('mandatory', 'False'),
500 ('mandatory', 'False'),
502 ('__end__', 'reviewer:mapping'),
501 ('__end__', 'reviewer:mapping'),
503 ('__end__', 'review_members:sequence'),
502 ('__end__', 'review_members:sequence'),
504 ('__start__', 'revisions:sequence'),
503 ('__start__', 'revisions:sequence'),
505 ('revisions', commit_ids['change']),
504 ('revisions', commit_ids['change']),
506 ('__end__', 'revisions:sequence'),
505 ('__end__', 'revisions:sequence'),
507 ('user', ''),
506 ('user', ''),
508 ('csrf_token', csrf_token),
507 ('csrf_token', csrf_token),
509 ],
508 ],
510 status=302)
509 status=302)
511
510
512 location = response.headers['Location']
511 location = response.headers['Location']
513
512
514 pull_request_id = location.rsplit('/', 1)[1]
513 pull_request_id = location.rsplit('/', 1)[1]
515 assert pull_request_id != 'new'
514 assert pull_request_id != 'new'
516 pull_request = PullRequest.get(int(pull_request_id))
515 pull_request = PullRequest.get(int(pull_request_id))
517
516
518 # target_ref has to point to the ancestor's commit_id in order to
517 # target_ref has to point to the ancestor's commit_id in order to
519 # show the correct diff
518 # show the correct diff
520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
519 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
521 assert pull_request.target_ref == expected_target_ref
520 assert pull_request.target_ref == expected_target_ref
522
521
523 # Check generated diff contents
522 # Check generated diff contents
524 response = response.follow()
523 response = response.follow()
525 assert 'content_of_ancestor' not in response.body
524 assert 'content_of_ancestor' not in response.body
526 assert 'content_of_ancestor-child' not in response.body
525 assert 'content_of_ancestor-child' not in response.body
527 assert 'content_of_change' in response.body
526 assert 'content_of_change' in response.body
528
527
529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
528 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
530 # Clear any previous calls to rcextensions
529 # Clear any previous calls to rcextensions
531 rhodecode.EXTENSIONS.calls.clear()
530 rhodecode.EXTENSIONS.calls.clear()
532
531
533 pull_request = pr_util.create_pull_request(
532 pull_request = pr_util.create_pull_request(
534 approved=True, mergeable=True)
533 approved=True, mergeable=True)
535 pull_request_id = pull_request.pull_request_id
534 pull_request_id = pull_request.pull_request_id
536 repo_name = pull_request.target_repo.scm_instance().name,
535 repo_name = pull_request.target_repo.scm_instance().name,
537
536
538 response = self.app.post(
537 response = self.app.post(
539 route_path('pullrequest_merge',
538 route_path('pullrequest_merge',
540 repo_name=str(repo_name[0]),
539 repo_name=str(repo_name[0]),
541 pull_request_id=pull_request_id),
540 pull_request_id=pull_request_id),
542 params={'csrf_token': csrf_token}).follow()
541 params={'csrf_token': csrf_token}).follow()
543
542
544 pull_request = PullRequest.get(pull_request_id)
543 pull_request = PullRequest.get(pull_request_id)
545
544
546 assert response.status_int == 200
545 assert response.status_int == 200
547 assert pull_request.is_closed()
546 assert pull_request.is_closed()
548 assert_pull_request_status(
547 assert_pull_request_status(
549 pull_request, ChangesetStatus.STATUS_APPROVED)
548 pull_request, ChangesetStatus.STATUS_APPROVED)
550
549
551 # Check the relevant log entries were added
550 # Check the relevant log entries were added
552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
551 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
553 actions = [log.action for log in user_logs]
552 actions = [log.action for log in user_logs]
554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
553 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
555 expected_actions = [
554 expected_actions = [
556 u'repo.pull_request.close',
555 u'repo.pull_request.close',
557 u'repo.pull_request.merge',
556 u'repo.pull_request.merge',
558 u'repo.pull_request.comment.create'
557 u'repo.pull_request.comment.create'
559 ]
558 ]
560 assert actions == expected_actions
559 assert actions == expected_actions
561
560
562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
561 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
563 actions = [log for log in user_logs]
562 actions = [log for log in user_logs]
564 assert actions[-1].action == 'user.push'
563 assert actions[-1].action == 'user.push'
565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
564 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
566
565
567 # Check post_push rcextension was really executed
566 # Check post_push rcextension was really executed
568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
567 push_calls = rhodecode.EXTENSIONS.calls['post_push']
569 assert len(push_calls) == 1
568 assert len(push_calls) == 1
570 unused_last_call_args, last_call_kwargs = push_calls[0]
569 unused_last_call_args, last_call_kwargs = push_calls[0]
571 assert last_call_kwargs['action'] == 'push'
570 assert last_call_kwargs['action'] == 'push'
572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
571 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
573
572
574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
573 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
575 pull_request = pr_util.create_pull_request(mergeable=False)
574 pull_request = pr_util.create_pull_request(mergeable=False)
576 pull_request_id = pull_request.pull_request_id
575 pull_request_id = pull_request.pull_request_id
577 pull_request = PullRequest.get(pull_request_id)
576 pull_request = PullRequest.get(pull_request_id)
578
577
579 response = self.app.post(
578 response = self.app.post(
580 route_path('pullrequest_merge',
579 route_path('pullrequest_merge',
581 repo_name=pull_request.target_repo.scm_instance().name,
580 repo_name=pull_request.target_repo.scm_instance().name,
582 pull_request_id=pull_request.pull_request_id),
581 pull_request_id=pull_request.pull_request_id),
583 params={'csrf_token': csrf_token}).follow()
582 params={'csrf_token': csrf_token}).follow()
584
583
585 assert response.status_int == 200
584 assert response.status_int == 200
586 response.mustcontain(
585 response.mustcontain(
587 'Merge is not currently possible because of below failed checks.')
586 'Merge is not currently possible because of below failed checks.')
588 response.mustcontain('Server-side pull request merging is disabled.')
587 response.mustcontain('Server-side pull request merging is disabled.')
589
588
590 @pytest.mark.skip_backends('svn')
589 @pytest.mark.skip_backends('svn')
591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
590 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
592 pull_request = pr_util.create_pull_request(mergeable=True)
591 pull_request = pr_util.create_pull_request(mergeable=True)
593 pull_request_id = pull_request.pull_request_id
592 pull_request_id = pull_request.pull_request_id
594 repo_name = pull_request.target_repo.scm_instance().name
593 repo_name = pull_request.target_repo.scm_instance().name
595
594
596 response = self.app.post(
595 response = self.app.post(
597 route_path('pullrequest_merge',
596 route_path('pullrequest_merge',
598 repo_name=repo_name,
597 repo_name=repo_name,
599 pull_request_id=pull_request_id),
598 pull_request_id=pull_request_id),
600 params={'csrf_token': csrf_token}).follow()
599 params={'csrf_token': csrf_token}).follow()
601
600
602 assert response.status_int == 200
601 assert response.status_int == 200
603
602
604 response.mustcontain(
603 response.mustcontain(
605 'Merge is not currently possible because of below failed checks.')
604 'Merge is not currently possible because of below failed checks.')
606 response.mustcontain('Pull request reviewer approval is pending.')
605 response.mustcontain('Pull request reviewer approval is pending.')
607
606
608 def test_merge_pull_request_renders_failure_reason(
607 def test_merge_pull_request_renders_failure_reason(
609 self, user_regular, csrf_token, pr_util):
608 self, user_regular, csrf_token, pr_util):
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
609 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 pull_request_id = pull_request.pull_request_id
610 pull_request_id = pull_request.pull_request_id
612 repo_name = pull_request.target_repo.scm_instance().name
611 repo_name = pull_request.target_repo.scm_instance().name
613
612
614 model_patcher = mock.patch.multiple(
613 model_patcher = mock.patch.multiple(
615 PullRequestModel,
614 PullRequestModel,
616 merge=mock.Mock(return_value=MergeResponse(
615 merge=mock.Mock(return_value=MergeResponse(
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
616 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
617 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619
618
620 with model_patcher:
619 with model_patcher:
621 response = self.app.post(
620 response = self.app.post(
622 route_path('pullrequest_merge',
621 route_path('pullrequest_merge',
623 repo_name=repo_name,
622 repo_name=repo_name,
624 pull_request_id=pull_request_id),
623 pull_request_id=pull_request_id),
625 params={'csrf_token': csrf_token}, status=302)
624 params={'csrf_token': csrf_token}, status=302)
626
625
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
626 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 MergeFailureReason.PUSH_FAILED])
627 MergeFailureReason.PUSH_FAILED])
629
628
630 def test_update_source_revision(self, backend, csrf_token):
629 def test_update_source_revision(self, backend, csrf_token):
631 commits = [
630 commits = [
632 {'message': 'ancestor'},
631 {'message': 'ancestor'},
633 {'message': 'change'},
632 {'message': 'change'},
634 {'message': 'change-2'},
633 {'message': 'change-2'},
635 ]
634 ]
636 commit_ids = backend.create_master_repo(commits)
635 commit_ids = backend.create_master_repo(commits)
637 target = backend.create_repo(heads=['ancestor'])
636 target = backend.create_repo(heads=['ancestor'])
638 source = backend.create_repo(heads=['change'])
637 source = backend.create_repo(heads=['change'])
639
638
640 # create pr from a in source to A in target
639 # create pr from a in source to A in target
641 pull_request = PullRequest()
640 pull_request = PullRequest()
642 pull_request.source_repo = source
641 pull_request.source_repo = source
643 # TODO: johbo: Make sure that we write the source ref this way!
642 # TODO: johbo: Make sure that we write the source ref this way!
644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
643 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
644 branch=backend.default_branch_name, commit_id=commit_ids['change'])
646 pull_request.target_repo = target
645 pull_request.target_repo = target
647
646
648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
647 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
649 branch=backend.default_branch_name,
648 branch=backend.default_branch_name,
650 commit_id=commit_ids['ancestor'])
649 commit_id=commit_ids['ancestor'])
651 pull_request.revisions = [commit_ids['change']]
650 pull_request.revisions = [commit_ids['change']]
652 pull_request.title = u"Test"
651 pull_request.title = u"Test"
653 pull_request.description = u"Description"
652 pull_request.description = u"Description"
654 pull_request.author = UserModel().get_by_username(
653 pull_request.author = UserModel().get_by_username(
655 TEST_USER_ADMIN_LOGIN)
654 TEST_USER_ADMIN_LOGIN)
656 Session().add(pull_request)
655 Session().add(pull_request)
657 Session().commit()
656 Session().commit()
658 pull_request_id = pull_request.pull_request_id
657 pull_request_id = pull_request.pull_request_id
659
658
660 # source has ancestor - change - change-2
659 # source has ancestor - change - change-2
661 backend.pull_heads(source, heads=['change-2'])
660 backend.pull_heads(source, heads=['change-2'])
662
661
663 # update PR
662 # update PR
664 self.app.post(
663 self.app.post(
665 route_path('pullrequest_update',
664 route_path('pullrequest_update',
666 repo_name=target.repo_name,
665 repo_name=target.repo_name,
667 pull_request_id=pull_request_id),
666 pull_request_id=pull_request_id),
668 params={'update_commits': 'true',
667 params={'update_commits': 'true',
669 'csrf_token': csrf_token})
668 'csrf_token': csrf_token})
670
669
671 # check that we have now both revisions
670 # check that we have now both revisions
672 pull_request = PullRequest.get(pull_request_id)
671 pull_request = PullRequest.get(pull_request_id)
673 assert pull_request.revisions == [
672 assert pull_request.revisions == [
674 commit_ids['change-2'], commit_ids['change']]
673 commit_ids['change-2'], commit_ids['change']]
675
674
676 # TODO: johbo: this should be a test on its own
675 # TODO: johbo: this should be a test on its own
677 response = self.app.get(route_path(
676 response = self.app.get(route_path(
678 'pullrequest_new',
677 'pullrequest_new',
679 repo_name=target.repo_name))
678 repo_name=target.repo_name))
680 assert response.status_int == 200
679 assert response.status_int == 200
681 assert 'Pull request updated to' in response.body
680 assert 'Pull request updated to' in response.body
682 assert 'with 1 added, 0 removed commits.' in response.body
681 assert 'with 1 added, 0 removed commits.' in response.body
683
682
684 def test_update_target_revision(self, backend, csrf_token):
683 def test_update_target_revision(self, backend, csrf_token):
685 commits = [
684 commits = [
686 {'message': 'ancestor'},
685 {'message': 'ancestor'},
687 {'message': 'change'},
686 {'message': 'change'},
688 {'message': 'ancestor-new', 'parents': ['ancestor']},
687 {'message': 'ancestor-new', 'parents': ['ancestor']},
689 {'message': 'change-rebased'},
688 {'message': 'change-rebased'},
690 ]
689 ]
691 commit_ids = backend.create_master_repo(commits)
690 commit_ids = backend.create_master_repo(commits)
692 target = backend.create_repo(heads=['ancestor'])
691 target = backend.create_repo(heads=['ancestor'])
693 source = backend.create_repo(heads=['change'])
692 source = backend.create_repo(heads=['change'])
694
693
695 # create pr from a in source to A in target
694 # create pr from a in source to A in target
696 pull_request = PullRequest()
695 pull_request = PullRequest()
697 pull_request.source_repo = source
696 pull_request.source_repo = source
698 # TODO: johbo: Make sure that we write the source ref this way!
697 # TODO: johbo: Make sure that we write the source ref this way!
699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
698 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
699 branch=backend.default_branch_name, commit_id=commit_ids['change'])
701 pull_request.target_repo = target
700 pull_request.target_repo = target
702 # TODO: johbo: Target ref should be branch based, since tip can jump
701 # TODO: johbo: Target ref should be branch based, since tip can jump
703 # from branch to branch
702 # from branch to branch
704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
703 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
705 branch=backend.default_branch_name,
704 branch=backend.default_branch_name,
706 commit_id=commit_ids['ancestor'])
705 commit_id=commit_ids['ancestor'])
707 pull_request.revisions = [commit_ids['change']]
706 pull_request.revisions = [commit_ids['change']]
708 pull_request.title = u"Test"
707 pull_request.title = u"Test"
709 pull_request.description = u"Description"
708 pull_request.description = u"Description"
710 pull_request.author = UserModel().get_by_username(
709 pull_request.author = UserModel().get_by_username(
711 TEST_USER_ADMIN_LOGIN)
710 TEST_USER_ADMIN_LOGIN)
712 Session().add(pull_request)
711 Session().add(pull_request)
713 Session().commit()
712 Session().commit()
714 pull_request_id = pull_request.pull_request_id
713 pull_request_id = pull_request.pull_request_id
715
714
716 # target has ancestor - ancestor-new
715 # target has ancestor - ancestor-new
717 # source has ancestor - ancestor-new - change-rebased
716 # source has ancestor - ancestor-new - change-rebased
718 backend.pull_heads(target, heads=['ancestor-new'])
717 backend.pull_heads(target, heads=['ancestor-new'])
719 backend.pull_heads(source, heads=['change-rebased'])
718 backend.pull_heads(source, heads=['change-rebased'])
720
719
721 # update PR
720 # update PR
722 self.app.post(
721 self.app.post(
723 route_path('pullrequest_update',
722 route_path('pullrequest_update',
724 repo_name=target.repo_name,
723 repo_name=target.repo_name,
725 pull_request_id=pull_request_id),
724 pull_request_id=pull_request_id),
726 params={'update_commits': 'true',
725 params={'update_commits': 'true',
727 'csrf_token': csrf_token},
726 'csrf_token': csrf_token},
728 status=200)
727 status=200)
729
728
730 # check that we have now both revisions
729 # check that we have now both revisions
731 pull_request = PullRequest.get(pull_request_id)
730 pull_request = PullRequest.get(pull_request_id)
732 assert pull_request.revisions == [commit_ids['change-rebased']]
731 assert pull_request.revisions == [commit_ids['change-rebased']]
733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
732 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
734 branch=backend.default_branch_name,
733 branch=backend.default_branch_name,
735 commit_id=commit_ids['ancestor-new'])
734 commit_id=commit_ids['ancestor-new'])
736
735
737 # TODO: johbo: This should be a test on its own
736 # TODO: johbo: This should be a test on its own
738 response = self.app.get(route_path(
737 response = self.app.get(route_path(
739 'pullrequest_new',
738 'pullrequest_new',
740 repo_name=target.repo_name))
739 repo_name=target.repo_name))
741 assert response.status_int == 200
740 assert response.status_int == 200
742 assert 'Pull request updated to' in response.body
741 assert 'Pull request updated to' in response.body
743 assert 'with 1 added, 1 removed commits.' in response.body
742 assert 'with 1 added, 1 removed commits.' in response.body
744
743
745 def test_update_of_ancestor_reference(self, backend, csrf_token):
744 def test_update_of_ancestor_reference(self, backend, csrf_token):
746 commits = [
745 commits = [
747 {'message': 'ancestor'},
746 {'message': 'ancestor'},
748 {'message': 'change'},
747 {'message': 'change'},
749 {'message': 'change-2'},
748 {'message': 'change-2'},
750 {'message': 'ancestor-new', 'parents': ['ancestor']},
749 {'message': 'ancestor-new', 'parents': ['ancestor']},
751 {'message': 'change-rebased'},
750 {'message': 'change-rebased'},
752 ]
751 ]
753 commit_ids = backend.create_master_repo(commits)
752 commit_ids = backend.create_master_repo(commits)
754 target = backend.create_repo(heads=['ancestor'])
753 target = backend.create_repo(heads=['ancestor'])
755 source = backend.create_repo(heads=['change'])
754 source = backend.create_repo(heads=['change'])
756
755
757 # create pr from a in source to A in target
756 # create pr from a in source to A in target
758 pull_request = PullRequest()
757 pull_request = PullRequest()
759 pull_request.source_repo = source
758 pull_request.source_repo = source
760 # TODO: johbo: Make sure that we write the source ref this way!
759 # TODO: johbo: Make sure that we write the source ref this way!
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
760 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
762 branch=backend.default_branch_name,
761 branch=backend.default_branch_name,
763 commit_id=commit_ids['change'])
762 commit_id=commit_ids['change'])
764 pull_request.target_repo = target
763 pull_request.target_repo = target
765 # TODO: johbo: Target ref should be branch based, since tip can jump
764 # TODO: johbo: Target ref should be branch based, since tip can jump
766 # from branch to branch
765 # from branch to branch
767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
766 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
768 branch=backend.default_branch_name,
767 branch=backend.default_branch_name,
769 commit_id=commit_ids['ancestor'])
768 commit_id=commit_ids['ancestor'])
770 pull_request.revisions = [commit_ids['change']]
769 pull_request.revisions = [commit_ids['change']]
771 pull_request.title = u"Test"
770 pull_request.title = u"Test"
772 pull_request.description = u"Description"
771 pull_request.description = u"Description"
773 pull_request.author = UserModel().get_by_username(
772 pull_request.author = UserModel().get_by_username(
774 TEST_USER_ADMIN_LOGIN)
773 TEST_USER_ADMIN_LOGIN)
775 Session().add(pull_request)
774 Session().add(pull_request)
776 Session().commit()
775 Session().commit()
777 pull_request_id = pull_request.pull_request_id
776 pull_request_id = pull_request.pull_request_id
778
777
779 # target has ancestor - ancestor-new
778 # target has ancestor - ancestor-new
780 # source has ancestor - ancestor-new - change-rebased
779 # source has ancestor - ancestor-new - change-rebased
781 backend.pull_heads(target, heads=['ancestor-new'])
780 backend.pull_heads(target, heads=['ancestor-new'])
782 backend.pull_heads(source, heads=['change-rebased'])
781 backend.pull_heads(source, heads=['change-rebased'])
783
782
784 # update PR
783 # update PR
785 self.app.post(
784 self.app.post(
786 route_path('pullrequest_update',
785 route_path('pullrequest_update',
787 repo_name=target.repo_name,
786 repo_name=target.repo_name,
788 pull_request_id=pull_request_id),
787 pull_request_id=pull_request_id),
789 params={'update_commits': 'true',
788 params={'update_commits': 'true',
790 'csrf_token': csrf_token},
789 'csrf_token': csrf_token},
791 status=200)
790 status=200)
792
791
793 # Expect the target reference to be updated correctly
792 # Expect the target reference to be updated correctly
794 pull_request = PullRequest.get(pull_request_id)
793 pull_request = PullRequest.get(pull_request_id)
795 assert pull_request.revisions == [commit_ids['change-rebased']]
794 assert pull_request.revisions == [commit_ids['change-rebased']]
796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
795 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
797 branch=backend.default_branch_name,
796 branch=backend.default_branch_name,
798 commit_id=commit_ids['ancestor-new'])
797 commit_id=commit_ids['ancestor-new'])
799 assert pull_request.target_ref == expected_target_ref
798 assert pull_request.target_ref == expected_target_ref
800
799
801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
800 def test_remove_pull_request_branch(self, backend_git, csrf_token):
802 branch_name = 'development'
801 branch_name = 'development'
803 commits = [
802 commits = [
804 {'message': 'initial-commit'},
803 {'message': 'initial-commit'},
805 {'message': 'old-feature'},
804 {'message': 'old-feature'},
806 {'message': 'new-feature', 'branch': branch_name},
805 {'message': 'new-feature', 'branch': branch_name},
807 ]
806 ]
808 repo = backend_git.create_repo(commits)
807 repo = backend_git.create_repo(commits)
809 commit_ids = backend_git.commit_ids
808 commit_ids = backend_git.commit_ids
810
809
811 pull_request = PullRequest()
810 pull_request = PullRequest()
812 pull_request.source_repo = repo
811 pull_request.source_repo = repo
813 pull_request.target_repo = repo
812 pull_request.target_repo = repo
814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
813 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
815 branch=branch_name, commit_id=commit_ids['new-feature'])
814 branch=branch_name, commit_id=commit_ids['new-feature'])
816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
815 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
817 branch=backend_git.default_branch_name,
816 branch=backend_git.default_branch_name,
818 commit_id=commit_ids['old-feature'])
817 commit_id=commit_ids['old-feature'])
819 pull_request.revisions = [commit_ids['new-feature']]
818 pull_request.revisions = [commit_ids['new-feature']]
820 pull_request.title = u"Test"
819 pull_request.title = u"Test"
821 pull_request.description = u"Description"
820 pull_request.description = u"Description"
822 pull_request.author = UserModel().get_by_username(
821 pull_request.author = UserModel().get_by_username(
823 TEST_USER_ADMIN_LOGIN)
822 TEST_USER_ADMIN_LOGIN)
824 Session().add(pull_request)
823 Session().add(pull_request)
825 Session().commit()
824 Session().commit()
826
825
827 vcs = repo.scm_instance()
826 vcs = repo.scm_instance()
828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
827 vcs.remove_ref('refs/heads/{}'.format(branch_name))
829
828
830 response = self.app.get(route_path(
829 response = self.app.get(route_path(
831 'pullrequest_show',
830 'pullrequest_show',
832 repo_name=repo.repo_name,
831 repo_name=repo.repo_name,
833 pull_request_id=pull_request.pull_request_id))
832 pull_request_id=pull_request.pull_request_id))
834
833
835 assert response.status_int == 200
834 assert response.status_int == 200
836 assert_response = AssertResponse(response)
835 assert_response = AssertResponse(response)
837 assert_response.element_contains(
836 assert_response.element_contains(
838 '#changeset_compare_view_content .alert strong',
837 '#changeset_compare_view_content .alert strong',
839 'Missing commits')
838 'Missing commits')
840 assert_response.element_contains(
839 assert_response.element_contains(
841 '#changeset_compare_view_content .alert',
840 '#changeset_compare_view_content .alert',
842 'This pull request cannot be displayed, because one or more'
841 'This pull request cannot be displayed, because one or more'
843 ' commits no longer exist in the source repository.')
842 ' commits no longer exist in the source repository.')
844
843
845 def test_strip_commits_from_pull_request(
844 def test_strip_commits_from_pull_request(
846 self, backend, pr_util, csrf_token):
845 self, backend, pr_util, csrf_token):
847 commits = [
846 commits = [
848 {'message': 'initial-commit'},
847 {'message': 'initial-commit'},
849 {'message': 'old-feature'},
848 {'message': 'old-feature'},
850 {'message': 'new-feature', 'parents': ['initial-commit']},
849 {'message': 'new-feature', 'parents': ['initial-commit']},
851 ]
850 ]
852 pull_request = pr_util.create_pull_request(
851 pull_request = pr_util.create_pull_request(
853 commits, target_head='initial-commit', source_head='new-feature',
852 commits, target_head='initial-commit', source_head='new-feature',
854 revisions=['new-feature'])
853 revisions=['new-feature'])
855
854
856 vcs = pr_util.source_repository.scm_instance()
855 vcs = pr_util.source_repository.scm_instance()
857 if backend.alias == 'git':
856 if backend.alias == 'git':
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
857 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
859 else:
858 else:
860 vcs.strip(pr_util.commit_ids['new-feature'])
859 vcs.strip(pr_util.commit_ids['new-feature'])
861
860
862 response = self.app.get(route_path(
861 response = self.app.get(route_path(
863 'pullrequest_show',
862 'pullrequest_show',
864 repo_name=pr_util.target_repository.repo_name,
863 repo_name=pr_util.target_repository.repo_name,
865 pull_request_id=pull_request.pull_request_id))
864 pull_request_id=pull_request.pull_request_id))
866
865
867 assert response.status_int == 200
866 assert response.status_int == 200
868 assert_response = AssertResponse(response)
867 assert_response = AssertResponse(response)
869 assert_response.element_contains(
868 assert_response.element_contains(
870 '#changeset_compare_view_content .alert strong',
869 '#changeset_compare_view_content .alert strong',
871 'Missing commits')
870 'Missing commits')
872 assert_response.element_contains(
871 assert_response.element_contains(
873 '#changeset_compare_view_content .alert',
872 '#changeset_compare_view_content .alert',
874 'This pull request cannot be displayed, because one or more'
873 'This pull request cannot be displayed, because one or more'
875 ' commits no longer exist in the source repository.')
874 ' commits no longer exist in the source repository.')
876 assert_response.element_contains(
875 assert_response.element_contains(
877 '#update_commits',
876 '#update_commits',
878 'Update commits')
877 'Update commits')
879
878
880 def test_strip_commits_and_update(
879 def test_strip_commits_and_update(
881 self, backend, pr_util, csrf_token):
880 self, backend, pr_util, csrf_token):
882 commits = [
881 commits = [
883 {'message': 'initial-commit'},
882 {'message': 'initial-commit'},
884 {'message': 'old-feature'},
883 {'message': 'old-feature'},
885 {'message': 'new-feature', 'parents': ['old-feature']},
884 {'message': 'new-feature', 'parents': ['old-feature']},
886 ]
885 ]
887 pull_request = pr_util.create_pull_request(
886 pull_request = pr_util.create_pull_request(
888 commits, target_head='old-feature', source_head='new-feature',
887 commits, target_head='old-feature', source_head='new-feature',
889 revisions=['new-feature'], mergeable=True)
888 revisions=['new-feature'], mergeable=True)
890
889
891 vcs = pr_util.source_repository.scm_instance()
890 vcs = pr_util.source_repository.scm_instance()
892 if backend.alias == 'git':
891 if backend.alias == 'git':
893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
892 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
894 else:
893 else:
895 vcs.strip(pr_util.commit_ids['new-feature'])
894 vcs.strip(pr_util.commit_ids['new-feature'])
896
895
897 response = self.app.post(
896 response = self.app.post(
898 route_path('pullrequest_update',
897 route_path('pullrequest_update',
899 repo_name=pull_request.target_repo.repo_name,
898 repo_name=pull_request.target_repo.repo_name,
900 pull_request_id=pull_request.pull_request_id),
899 pull_request_id=pull_request.pull_request_id),
901 params={'update_commits': 'true',
900 params={'update_commits': 'true',
902 'csrf_token': csrf_token})
901 'csrf_token': csrf_token})
903
902
904 assert response.status_int == 200
903 assert response.status_int == 200
905 assert response.body == 'true'
904 assert response.body == 'true'
906
905
907 # Make sure that after update, it won't raise 500 errors
906 # Make sure that after update, it won't raise 500 errors
908 response = self.app.get(route_path(
907 response = self.app.get(route_path(
909 'pullrequest_show',
908 'pullrequest_show',
910 repo_name=pr_util.target_repository.repo_name,
909 repo_name=pr_util.target_repository.repo_name,
911 pull_request_id=pull_request.pull_request_id))
910 pull_request_id=pull_request.pull_request_id))
912
911
913 assert response.status_int == 200
912 assert response.status_int == 200
914 assert_response = AssertResponse(response)
913 assert_response = AssertResponse(response)
915 assert_response.element_contains(
914 assert_response.element_contains(
916 '#changeset_compare_view_content .alert strong',
915 '#changeset_compare_view_content .alert strong',
917 'Missing commits')
916 'Missing commits')
918
917
919 def test_branch_is_a_link(self, pr_util):
918 def test_branch_is_a_link(self, pr_util):
920 pull_request = pr_util.create_pull_request()
919 pull_request = pr_util.create_pull_request()
921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
920 pull_request.source_ref = 'branch:origin:1234567890abcdef'
922 pull_request.target_ref = 'branch:target:abcdef1234567890'
921 pull_request.target_ref = 'branch:target:abcdef1234567890'
923 Session().add(pull_request)
922 Session().add(pull_request)
924 Session().commit()
923 Session().commit()
925
924
926 response = self.app.get(route_path(
925 response = self.app.get(route_path(
927 'pullrequest_show',
926 'pullrequest_show',
928 repo_name=pull_request.target_repo.scm_instance().name,
927 repo_name=pull_request.target_repo.scm_instance().name,
929 pull_request_id=pull_request.pull_request_id))
928 pull_request_id=pull_request.pull_request_id))
930 assert response.status_int == 200
929 assert response.status_int == 200
931 assert_response = AssertResponse(response)
930 assert_response = AssertResponse(response)
932
931
933 origin = assert_response.get_element('.pr-origininfo .tag')
932 origin = assert_response.get_element('.pr-origininfo .tag')
934 origin_children = origin.getchildren()
933 origin_children = origin.getchildren()
935 assert len(origin_children) == 1
934 assert len(origin_children) == 1
936 target = assert_response.get_element('.pr-targetinfo .tag')
935 target = assert_response.get_element('.pr-targetinfo .tag')
937 target_children = target.getchildren()
936 target_children = target.getchildren()
938 assert len(target_children) == 1
937 assert len(target_children) == 1
939
938
940 expected_origin_link = route_path(
939 expected_origin_link = route_path(
941 'repo_changelog',
940 'repo_changelog',
942 repo_name=pull_request.source_repo.scm_instance().name,
941 repo_name=pull_request.source_repo.scm_instance().name,
943 params=dict(branch='origin'))
942 params=dict(branch='origin'))
944 expected_target_link = route_path(
943 expected_target_link = route_path(
945 'repo_changelog',
944 'repo_changelog',
946 repo_name=pull_request.target_repo.scm_instance().name,
945 repo_name=pull_request.target_repo.scm_instance().name,
947 params=dict(branch='target'))
946 params=dict(branch='target'))
948 assert origin_children[0].attrib['href'] == expected_origin_link
947 assert origin_children[0].attrib['href'] == expected_origin_link
949 assert origin_children[0].text == 'branch: origin'
948 assert origin_children[0].text == 'branch: origin'
950 assert target_children[0].attrib['href'] == expected_target_link
949 assert target_children[0].attrib['href'] == expected_target_link
951 assert target_children[0].text == 'branch: target'
950 assert target_children[0].text == 'branch: target'
952
951
953 def test_bookmark_is_not_a_link(self, pr_util):
952 def test_bookmark_is_not_a_link(self, pr_util):
954 pull_request = pr_util.create_pull_request()
953 pull_request = pr_util.create_pull_request()
955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
954 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
955 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
957 Session().add(pull_request)
956 Session().add(pull_request)
958 Session().commit()
957 Session().commit()
959
958
960 response = self.app.get(route_path(
959 response = self.app.get(route_path(
961 'pullrequest_show',
960 'pullrequest_show',
962 repo_name=pull_request.target_repo.scm_instance().name,
961 repo_name=pull_request.target_repo.scm_instance().name,
963 pull_request_id=pull_request.pull_request_id))
962 pull_request_id=pull_request.pull_request_id))
964 assert response.status_int == 200
963 assert response.status_int == 200
965 assert_response = AssertResponse(response)
964 assert_response = AssertResponse(response)
966
965
967 origin = assert_response.get_element('.pr-origininfo .tag')
966 origin = assert_response.get_element('.pr-origininfo .tag')
968 assert origin.text.strip() == 'bookmark: origin'
967 assert origin.text.strip() == 'bookmark: origin'
969 assert origin.getchildren() == []
968 assert origin.getchildren() == []
970
969
971 target = assert_response.get_element('.pr-targetinfo .tag')
970 target = assert_response.get_element('.pr-targetinfo .tag')
972 assert target.text.strip() == 'bookmark: target'
971 assert target.text.strip() == 'bookmark: target'
973 assert target.getchildren() == []
972 assert target.getchildren() == []
974
973
975 def test_tag_is_not_a_link(self, pr_util):
974 def test_tag_is_not_a_link(self, pr_util):
976 pull_request = pr_util.create_pull_request()
975 pull_request = pr_util.create_pull_request()
977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
976 pull_request.source_ref = 'tag:origin:1234567890abcdef'
978 pull_request.target_ref = 'tag:target:abcdef1234567890'
977 pull_request.target_ref = 'tag:target:abcdef1234567890'
979 Session().add(pull_request)
978 Session().add(pull_request)
980 Session().commit()
979 Session().commit()
981
980
982 response = self.app.get(route_path(
981 response = self.app.get(route_path(
983 'pullrequest_show',
982 'pullrequest_show',
984 repo_name=pull_request.target_repo.scm_instance().name,
983 repo_name=pull_request.target_repo.scm_instance().name,
985 pull_request_id=pull_request.pull_request_id))
984 pull_request_id=pull_request.pull_request_id))
986 assert response.status_int == 200
985 assert response.status_int == 200
987 assert_response = AssertResponse(response)
986 assert_response = AssertResponse(response)
988
987
989 origin = assert_response.get_element('.pr-origininfo .tag')
988 origin = assert_response.get_element('.pr-origininfo .tag')
990 assert origin.text.strip() == 'tag: origin'
989 assert origin.text.strip() == 'tag: origin'
991 assert origin.getchildren() == []
990 assert origin.getchildren() == []
992
991
993 target = assert_response.get_element('.pr-targetinfo .tag')
992 target = assert_response.get_element('.pr-targetinfo .tag')
994 assert target.text.strip() == 'tag: target'
993 assert target.text.strip() == 'tag: target'
995 assert target.getchildren() == []
994 assert target.getchildren() == []
996
995
997 @pytest.mark.parametrize('mergeable', [True, False])
996 @pytest.mark.parametrize('mergeable', [True, False])
998 def test_shadow_repository_link(
997 def test_shadow_repository_link(
999 self, mergeable, pr_util, http_host_only_stub):
998 self, mergeable, pr_util, http_host_only_stub):
1000 """
999 """
1001 Check that the pull request summary page displays a link to the shadow
1000 Check that the pull request summary page displays a link to the shadow
1002 repository if the pull request is mergeable. If it is not mergeable
1001 repository if the pull request is mergeable. If it is not mergeable
1003 the link should not be displayed.
1002 the link should not be displayed.
1004 """
1003 """
1005 pull_request = pr_util.create_pull_request(
1004 pull_request = pr_util.create_pull_request(
1006 mergeable=mergeable, enable_notifications=False)
1005 mergeable=mergeable, enable_notifications=False)
1007 target_repo = pull_request.target_repo.scm_instance()
1006 target_repo = pull_request.target_repo.scm_instance()
1008 pr_id = pull_request.pull_request_id
1007 pr_id = pull_request.pull_request_id
1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1008 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1009 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1011
1010
1012 response = self.app.get(route_path(
1011 response = self.app.get(route_path(
1013 'pullrequest_show',
1012 'pullrequest_show',
1014 repo_name=target_repo.name,
1013 repo_name=target_repo.name,
1015 pull_request_id=pr_id))
1014 pull_request_id=pr_id))
1016
1015
1017 assertr = AssertResponse(response)
1016 assertr = AssertResponse(response)
1018 if mergeable:
1017 if mergeable:
1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1018 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1019 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1021 else:
1020 else:
1022 assertr.no_element_exists('.pr-mergeinfo')
1021 assertr.no_element_exists('.pr-mergeinfo')
1023
1022
1024
1023
1025 @pytest.mark.usefixtures('app')
1024 @pytest.mark.usefixtures('app')
1026 @pytest.mark.backends("git", "hg")
1025 @pytest.mark.backends("git", "hg")
1027 class TestPullrequestsControllerDelete(object):
1026 class TestPullrequestsControllerDelete(object):
1028 def test_pull_request_delete_button_permissions_admin(
1027 def test_pull_request_delete_button_permissions_admin(
1029 self, autologin_user, user_admin, pr_util):
1028 self, autologin_user, user_admin, pr_util):
1030 pull_request = pr_util.create_pull_request(
1029 pull_request = pr_util.create_pull_request(
1031 author=user_admin.username, enable_notifications=False)
1030 author=user_admin.username, enable_notifications=False)
1032
1031
1033 response = self.app.get(route_path(
1032 response = self.app.get(route_path(
1034 'pullrequest_show',
1033 'pullrequest_show',
1035 repo_name=pull_request.target_repo.scm_instance().name,
1034 repo_name=pull_request.target_repo.scm_instance().name,
1036 pull_request_id=pull_request.pull_request_id))
1035 pull_request_id=pull_request.pull_request_id))
1037
1036
1038 response.mustcontain('id="delete_pullrequest"')
1037 response.mustcontain('id="delete_pullrequest"')
1039 response.mustcontain('Confirm to delete this pull request')
1038 response.mustcontain('Confirm to delete this pull request')
1040
1039
1041 def test_pull_request_delete_button_permissions_owner(
1040 def test_pull_request_delete_button_permissions_owner(
1042 self, autologin_regular_user, user_regular, pr_util):
1041 self, autologin_regular_user, user_regular, pr_util):
1043 pull_request = pr_util.create_pull_request(
1042 pull_request = pr_util.create_pull_request(
1044 author=user_regular.username, enable_notifications=False)
1043 author=user_regular.username, enable_notifications=False)
1045
1044
1046 response = self.app.get(route_path(
1045 response = self.app.get(route_path(
1047 'pullrequest_show',
1046 'pullrequest_show',
1048 repo_name=pull_request.target_repo.scm_instance().name,
1047 repo_name=pull_request.target_repo.scm_instance().name,
1049 pull_request_id=pull_request.pull_request_id))
1048 pull_request_id=pull_request.pull_request_id))
1050
1049
1051 response.mustcontain('id="delete_pullrequest"')
1050 response.mustcontain('id="delete_pullrequest"')
1052 response.mustcontain('Confirm to delete this pull request')
1051 response.mustcontain('Confirm to delete this pull request')
1053
1052
1054 def test_pull_request_delete_button_permissions_forbidden(
1053 def test_pull_request_delete_button_permissions_forbidden(
1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1054 self, autologin_regular_user, user_regular, user_admin, pr_util):
1056 pull_request = pr_util.create_pull_request(
1055 pull_request = pr_util.create_pull_request(
1057 author=user_admin.username, enable_notifications=False)
1056 author=user_admin.username, enable_notifications=False)
1058
1057
1059 response = self.app.get(route_path(
1058 response = self.app.get(route_path(
1060 'pullrequest_show',
1059 'pullrequest_show',
1061 repo_name=pull_request.target_repo.scm_instance().name,
1060 repo_name=pull_request.target_repo.scm_instance().name,
1062 pull_request_id=pull_request.pull_request_id))
1061 pull_request_id=pull_request.pull_request_id))
1063 response.mustcontain(no=['id="delete_pullrequest"'])
1062 response.mustcontain(no=['id="delete_pullrequest"'])
1064 response.mustcontain(no=['Confirm to delete this pull request'])
1063 response.mustcontain(no=['Confirm to delete this pull request'])
1065
1064
1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1065 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1066 self, autologin_regular_user, user_regular, user_admin, pr_util,
1068 user_util):
1067 user_util):
1069
1068
1070 pull_request = pr_util.create_pull_request(
1069 pull_request = pr_util.create_pull_request(
1071 author=user_admin.username, enable_notifications=False)
1070 author=user_admin.username, enable_notifications=False)
1072
1071
1073 user_util.grant_user_permission_to_repo(
1072 user_util.grant_user_permission_to_repo(
1074 pull_request.target_repo, user_regular,
1073 pull_request.target_repo, user_regular,
1075 'repository.write')
1074 'repository.write')
1076
1075
1077 response = self.app.get(route_path(
1076 response = self.app.get(route_path(
1078 'pullrequest_show',
1077 'pullrequest_show',
1079 repo_name=pull_request.target_repo.scm_instance().name,
1078 repo_name=pull_request.target_repo.scm_instance().name,
1080 pull_request_id=pull_request.pull_request_id))
1079 pull_request_id=pull_request.pull_request_id))
1081
1080
1082 response.mustcontain('id="open_edit_pullrequest"')
1081 response.mustcontain('id="open_edit_pullrequest"')
1083 response.mustcontain('id="delete_pullrequest"')
1082 response.mustcontain('id="delete_pullrequest"')
1084 response.mustcontain(no=['Confirm to delete this pull request'])
1083 response.mustcontain(no=['Confirm to delete this pull request'])
1085
1084
1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1085 def test_delete_comment_returns_404_if_comment_does_not_exist(
1087 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1086 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1088
1087
1089 pull_request = pr_util.create_pull_request(
1088 pull_request = pr_util.create_pull_request(
1090 author=user_admin.username, enable_notifications=False)
1089 author=user_admin.username, enable_notifications=False)
1091
1090
1092 self.app.post(
1091 self.app.post(
1093 route_path(
1092 route_path(
1094 'pullrequest_comment_delete',
1093 'pullrequest_comment_delete',
1095 repo_name=pull_request.target_repo.scm_instance().name,
1094 repo_name=pull_request.target_repo.scm_instance().name,
1096 pull_request_id=pull_request.pull_request_id,
1095 pull_request_id=pull_request.pull_request_id,
1097 comment_id=1024404),
1096 comment_id=1024404),
1098 extra_environ=xhr_header,
1097 extra_environ=xhr_header,
1099 params={'csrf_token': csrf_token},
1098 params={'csrf_token': csrf_token},
1100 status=404
1099 status=404
1101 )
1100 )
1102
1101
1103 def test_delete_comment(
1102 def test_delete_comment(
1104 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1103 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1105
1104
1106 pull_request = pr_util.create_pull_request(
1105 pull_request = pr_util.create_pull_request(
1107 author=user_admin.username, enable_notifications=False)
1106 author=user_admin.username, enable_notifications=False)
1108 comment = pr_util.create_comment()
1107 comment = pr_util.create_comment()
1109 comment_id = comment.comment_id
1108 comment_id = comment.comment_id
1110
1109
1111 response = self.app.post(
1110 response = self.app.post(
1112 route_path(
1111 route_path(
1113 'pullrequest_comment_delete',
1112 'pullrequest_comment_delete',
1114 repo_name=pull_request.target_repo.scm_instance().name,
1113 repo_name=pull_request.target_repo.scm_instance().name,
1115 pull_request_id=pull_request.pull_request_id,
1114 pull_request_id=pull_request.pull_request_id,
1116 comment_id=comment_id),
1115 comment_id=comment_id),
1117 extra_environ=xhr_header,
1116 extra_environ=xhr_header,
1118 params={'csrf_token': csrf_token},
1117 params={'csrf_token': csrf_token},
1119 status=200
1118 status=200
1120 )
1119 )
1121 assert response.body == 'true'
1120 assert response.body == 'true'
1122
1121
1123
1122
1124 def assert_pull_request_status(pull_request, expected_status):
1123 def assert_pull_request_status(pull_request, expected_status):
1125 status = ChangesetStatusModel().calculated_review_status(
1124 status = ChangesetStatusModel().calculated_review_status(
1126 pull_request=pull_request)
1125 pull_request=pull_request)
1127 assert status == expected_status
1126 assert status == expected_status
1128
1127
1129
1128
1130 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1129 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1131 @pytest.mark.usefixtures("autologin_user")
1130 @pytest.mark.usefixtures("autologin_user")
1132 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1131 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1133 response = app.get(
1132 response = app.get(
1134 route_path(route, repo_name=backend_svn.repo_name), status=404)
1133 route_path(route, repo_name=backend_svn.repo_name), status=404)
1135
1134
@@ -1,1251 +1,1259 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63
63
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'rhodecode:templates/data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176 self.load_default_context()
176 self.load_default_context()
177
177
178 # additional filters
178 # additional filters
179 req_get = self.request.GET
179 req_get = self.request.GET
180 source = str2bool(req_get.get('source'))
180 source = str2bool(req_get.get('source'))
181 closed = str2bool(req_get.get('closed'))
181 closed = str2bool(req_get.get('closed'))
182 my = str2bool(req_get.get('my'))
182 my = str2bool(req_get.get('my'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185
185
186 filter_type = 'awaiting_review' if awaiting_review \
186 filter_type = 'awaiting_review' if awaiting_review \
187 else 'awaiting_my_review' if awaiting_my_review \
187 else 'awaiting_my_review' if awaiting_my_review \
188 else None
188 else None
189
189
190 opened_by = None
190 opened_by = None
191 if my:
191 if my:
192 opened_by = [self._rhodecode_user.user_id]
192 opened_by = [self._rhodecode_user.user_id]
193
193
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 if closed:
195 if closed:
196 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
197
197
198 data = self._get_pull_requests_list(
198 data = self._get_pull_requests_list(
199 repo_name=self.db_repo_name, source=source,
199 repo_name=self.db_repo_name, source=source,
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201
201
202 return data
202 return data
203
203
204 def _get_pr_version(self, pull_request_id, version=None):
204 def _get_pr_version(self, pull_request_id, version=None):
205 at_version = None
205 at_version = None
206
206
207 if version and version == 'latest':
207 if version and version == 'latest':
208 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_ver = PullRequest.get(pull_request_id)
209 pull_request_obj = pull_request_ver
209 pull_request_obj = pull_request_ver
210 _org_pull_request_obj = pull_request_obj
210 _org_pull_request_obj = pull_request_obj
211 at_version = 'latest'
211 at_version = 'latest'
212 elif version:
212 elif version:
213 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_ver = PullRequestVersion.get_or_404(version)
214 pull_request_obj = pull_request_ver
214 pull_request_obj = pull_request_ver
215 _org_pull_request_obj = pull_request_ver.pull_request
215 _org_pull_request_obj = pull_request_ver.pull_request
216 at_version = pull_request_ver.pull_request_version_id
216 at_version = pull_request_ver.pull_request_version_id
217 else:
217 else:
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
219 pull_request_id)
219 pull_request_id)
220
220
221 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_display_obj = PullRequest.get_pr_display_object(
222 pull_request_obj, _org_pull_request_obj)
222 pull_request_obj, _org_pull_request_obj)
223
223
224 return _org_pull_request_obj, pull_request_obj, \
224 return _org_pull_request_obj, pull_request_obj, \
225 pull_request_display_obj, at_version
225 pull_request_display_obj, at_version
226
226
227 def _get_diffset(self, source_repo_name, source_repo,
227 def _get_diffset(self, source_repo_name, source_repo,
228 source_ref_id, target_ref_id,
228 source_ref_id, target_ref_id,
229 target_commit, source_commit, diff_limit, fulldiff,
229 target_commit, source_commit, diff_limit, fulldiff,
230 file_limit, display_inline_comments):
230 file_limit, display_inline_comments):
231
231
232 vcs_diff = PullRequestModel().get_diff(
232 vcs_diff = PullRequestModel().get_diff(
233 source_repo, source_ref_id, target_ref_id)
233 source_repo, source_ref_id, target_ref_id)
234
234
235 diff_processor = diffs.DiffProcessor(
235 diff_processor = diffs.DiffProcessor(
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
237 file_limit=file_limit, show_full_diff=fulldiff)
237 file_limit=file_limit, show_full_diff=fulldiff)
238
238
239 _parsed = diff_processor.prepare()
239 _parsed = diff_processor.prepare()
240
240
241 def _node_getter(commit):
241 def _node_getter(commit):
242 def get_node(fname):
242 def get_node(fname):
243 try:
243 try:
244 return commit.get_node(fname)
244 return commit.get_node(fname)
245 except NodeDoesNotExistError:
245 except NodeDoesNotExistError:
246 return None
246 return None
247
247
248 return get_node
248 return get_node
249
249
250 diffset = codeblocks.DiffSet(
250 diffset = codeblocks.DiffSet(
251 repo_name=self.db_repo_name,
251 repo_name=self.db_repo_name,
252 source_repo_name=source_repo_name,
252 source_repo_name=source_repo_name,
253 source_node_getter=_node_getter(target_commit),
253 source_node_getter=_node_getter(target_commit),
254 target_node_getter=_node_getter(source_commit),
254 target_node_getter=_node_getter(source_commit),
255 comments=display_inline_comments
255 comments=display_inline_comments
256 )
256 )
257 diffset = diffset.render_patchset(
257 diffset = diffset.render_patchset(
258 _parsed, target_commit.raw_id, source_commit.raw_id)
258 _parsed, target_commit.raw_id, source_commit.raw_id)
259
259
260 return diffset
260 return diffset
261
261
262 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
265 @view_config(
266 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
268 def pull_request_show(self):
269 pull_request_id = self.request.matchdict['pull_request_id']
269 pull_request_id = self.request.matchdict['pull_request_id']
270
270
271 c = self.load_default_context()
271 c = self.load_default_context()
272
272
273 version = self.request.GET.get('version')
273 version = self.request.GET.get('version')
274 from_version = self.request.GET.get('from_version') or version
274 from_version = self.request.GET.get('from_version') or version
275 merge_checks = self.request.GET.get('merge_checks')
275 merge_checks = self.request.GET.get('merge_checks')
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
277
277
278 (pull_request_latest,
278 (pull_request_latest,
279 pull_request_at_ver,
279 pull_request_at_ver,
280 pull_request_display_obj,
280 pull_request_display_obj,
281 at_version) = self._get_pr_version(
281 at_version) = self._get_pr_version(
282 pull_request_id, version=version)
282 pull_request_id, version=version)
283 pr_closed = pull_request_latest.is_closed()
283 pr_closed = pull_request_latest.is_closed()
284
284
285 if pr_closed and (version or from_version):
285 if pr_closed and (version or from_version):
286 # not allow to browse versions
286 # not allow to browse versions
287 raise HTTPFound(h.route_path(
287 raise HTTPFound(h.route_path(
288 'pullrequest_show', repo_name=self.db_repo_name,
288 'pullrequest_show', repo_name=self.db_repo_name,
289 pull_request_id=pull_request_id))
289 pull_request_id=pull_request_id))
290
290
291 versions = pull_request_display_obj.versions()
291 versions = pull_request_display_obj.versions()
292
292
293 c.at_version = at_version
293 c.at_version = at_version
294 c.at_version_num = (at_version
294 c.at_version_num = (at_version
295 if at_version and at_version != 'latest'
295 if at_version and at_version != 'latest'
296 else None)
296 else None)
297 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_pos = ChangesetComment.get_index_from_version(
298 c.at_version_num, versions)
298 c.at_version_num, versions)
299
299
300 (prev_pull_request_latest,
300 (prev_pull_request_latest,
301 prev_pull_request_at_ver,
301 prev_pull_request_at_ver,
302 prev_pull_request_display_obj,
302 prev_pull_request_display_obj,
303 prev_at_version) = self._get_pr_version(
303 prev_at_version) = self._get_pr_version(
304 pull_request_id, version=from_version)
304 pull_request_id, version=from_version)
305
305
306 c.from_version = prev_at_version
306 c.from_version = prev_at_version
307 c.from_version_num = (prev_at_version
307 c.from_version_num = (prev_at_version
308 if prev_at_version and prev_at_version != 'latest'
308 if prev_at_version and prev_at_version != 'latest'
309 else None)
309 else None)
310 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_pos = ChangesetComment.get_index_from_version(
311 c.from_version_num, versions)
311 c.from_version_num, versions)
312
312
313 # define if we're in COMPARE mode or VIEW at version mode
313 # define if we're in COMPARE mode or VIEW at version mode
314 compare = at_version != prev_at_version
314 compare = at_version != prev_at_version
315
315
316 # pull_requests repo_name we opened it against
316 # pull_requests repo_name we opened it against
317 # ie. target_repo must match
317 # ie. target_repo must match
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
319 raise HTTPNotFound()
319 raise HTTPNotFound()
320
320
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
322 pull_request_at_ver)
322 pull_request_at_ver)
323
323
324 c.pull_request = pull_request_display_obj
324 c.pull_request = pull_request_display_obj
325 c.pull_request_latest = pull_request_latest
325 c.pull_request_latest = pull_request_latest
326
326
327 if compare or (at_version and not at_version == 'latest'):
327 if compare or (at_version and not at_version == 'latest'):
328 c.allowed_to_change_status = False
328 c.allowed_to_change_status = False
329 c.allowed_to_update = False
329 c.allowed_to_update = False
330 c.allowed_to_merge = False
330 c.allowed_to_merge = False
331 c.allowed_to_delete = False
331 c.allowed_to_delete = False
332 c.allowed_to_comment = False
332 c.allowed_to_comment = False
333 c.allowed_to_close = False
333 c.allowed_to_close = False
334 else:
334 else:
335 can_change_status = PullRequestModel().check_user_change_status(
335 can_change_status = PullRequestModel().check_user_change_status(
336 pull_request_at_ver, self._rhodecode_user)
336 pull_request_at_ver, self._rhodecode_user)
337 c.allowed_to_change_status = can_change_status and not pr_closed
337 c.allowed_to_change_status = can_change_status and not pr_closed
338
338
339 c.allowed_to_update = PullRequestModel().check_user_update(
339 c.allowed_to_update = PullRequestModel().check_user_update(
340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 pull_request_latest, self._rhodecode_user) and not pr_closed
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 pull_request_latest, self._rhodecode_user) and not pr_closed
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
344 pull_request_latest, self._rhodecode_user) and not pr_closed
344 pull_request_latest, self._rhodecode_user) and not pr_closed
345 c.allowed_to_comment = not pr_closed
345 c.allowed_to_comment = not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
347
347
348 c.forbid_adding_reviewers = False
348 c.forbid_adding_reviewers = False
349 c.forbid_author_to_review = False
349 c.forbid_author_to_review = False
350 c.forbid_commit_author_to_review = False
350 c.forbid_commit_author_to_review = False
351
351
352 if pull_request_latest.reviewer_data and \
352 if pull_request_latest.reviewer_data and \
353 'rules' in pull_request_latest.reviewer_data:
353 'rules' in pull_request_latest.reviewer_data:
354 rules = pull_request_latest.reviewer_data['rules'] or {}
354 rules = pull_request_latest.reviewer_data['rules'] or {}
355 try:
355 try:
356 c.forbid_adding_reviewers = rules.get(
356 c.forbid_adding_reviewers = rules.get(
357 'forbid_adding_reviewers')
357 'forbid_adding_reviewers')
358 c.forbid_author_to_review = rules.get(
358 c.forbid_author_to_review = rules.get(
359 'forbid_author_to_review')
359 'forbid_author_to_review')
360 c.forbid_commit_author_to_review = rules.get(
360 c.forbid_commit_author_to_review = rules.get(
361 'forbid_commit_author_to_review')
361 'forbid_commit_author_to_review')
362 except Exception:
362 except Exception:
363 pass
363 pass
364
364
365 # check merge capabilities
365 # check merge capabilities
366 _merge_check = MergeCheck.validate(
366 _merge_check = MergeCheck.validate(
367 pull_request_latest, user=self._rhodecode_user,
367 pull_request_latest, user=self._rhodecode_user,
368 translator=self.request.translate)
368 translator=self.request.translate)
369 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_errors = _merge_check.error_details
370 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_possible = not _merge_check.failed
371 c.pr_merge_message = _merge_check.merge_msg
371 c.pr_merge_message = _merge_check.merge_msg
372
372
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
374 pull_request_latest, translator=self.request.translate)
374 pull_request_latest, translator=self.request.translate)
375
375
376 c.pull_request_review_status = _merge_check.review_status
376 c.pull_request_review_status = _merge_check.review_status
377 if merge_checks:
377 if merge_checks:
378 self.request.override_renderer = \
378 self.request.override_renderer = \
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
380 return self._get_template_context(c)
380 return self._get_template_context(c)
381
381
382 comments_model = CommentsModel()
382 comments_model = CommentsModel()
383
383
384 # reviewers and statuses
384 # reviewers and statuses
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
387
387
388 # GENERAL COMMENTS with versions #
388 # GENERAL COMMENTS with versions #
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
390 q = q.order_by(ChangesetComment.comment_id.asc())
390 q = q.order_by(ChangesetComment.comment_id.asc())
391 general_comments = q
391 general_comments = q
392
392
393 # pick comments we want to render at current version
393 # pick comments we want to render at current version
394 c.comment_versions = comments_model.aggregate_comments(
394 c.comment_versions = comments_model.aggregate_comments(
395 general_comments, versions, c.at_version_num)
395 general_comments, versions, c.at_version_num)
396 c.comments = c.comment_versions[c.at_version_num]['until']
396 c.comments = c.comment_versions[c.at_version_num]['until']
397
397
398 # INLINE COMMENTS with versions #
398 # INLINE COMMENTS with versions #
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
400 q = q.order_by(ChangesetComment.comment_id.asc())
400 q = q.order_by(ChangesetComment.comment_id.asc())
401 inline_comments = q
401 inline_comments = q
402
402
403 c.inline_versions = comments_model.aggregate_comments(
403 c.inline_versions = comments_model.aggregate_comments(
404 inline_comments, versions, c.at_version_num, inline=True)
404 inline_comments, versions, c.at_version_num, inline=True)
405
405
406 # inject latest version
406 # inject latest version
407 latest_ver = PullRequest.get_pr_display_object(
407 latest_ver = PullRequest.get_pr_display_object(
408 pull_request_latest, pull_request_latest)
408 pull_request_latest, pull_request_latest)
409
409
410 c.versions = versions + [latest_ver]
410 c.versions = versions + [latest_ver]
411
411
412 # if we use version, then do not show later comments
412 # if we use version, then do not show later comments
413 # than current version
413 # than current version
414 display_inline_comments = collections.defaultdict(
414 display_inline_comments = collections.defaultdict(
415 lambda: collections.defaultdict(list))
415 lambda: collections.defaultdict(list))
416 for co in inline_comments:
416 for co in inline_comments:
417 if c.at_version_num:
417 if c.at_version_num:
418 # pick comments that are at least UPTO given version, so we
418 # pick comments that are at least UPTO given version, so we
419 # don't render comments for higher version
419 # don't render comments for higher version
420 should_render = co.pull_request_version_id and \
420 should_render = co.pull_request_version_id and \
421 co.pull_request_version_id <= c.at_version_num
421 co.pull_request_version_id <= c.at_version_num
422 else:
422 else:
423 # showing all, for 'latest'
423 # showing all, for 'latest'
424 should_render = True
424 should_render = True
425
425
426 if should_render:
426 if should_render:
427 display_inline_comments[co.f_path][co.line_no].append(co)
427 display_inline_comments[co.f_path][co.line_no].append(co)
428
428
429 # load diff data into template context, if we use compare mode then
429 # load diff data into template context, if we use compare mode then
430 # diff is calculated based on changes between versions of PR
430 # diff is calculated based on changes between versions of PR
431
431
432 source_repo = pull_request_at_ver.source_repo
432 source_repo = pull_request_at_ver.source_repo
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
434
434
435 target_repo = pull_request_at_ver.target_repo
435 target_repo = pull_request_at_ver.target_repo
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
437
437
438 if compare:
438 if compare:
439 # in compare switch the diff base to latest commit from prev version
439 # in compare switch the diff base to latest commit from prev version
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
441
441
442 # despite opening commits for bookmarks/branches/tags, we always
442 # despite opening commits for bookmarks/branches/tags, we always
443 # convert this to rev to prevent changes after bookmark or branch change
443 # convert this to rev to prevent changes after bookmark or branch change
444 c.source_ref_type = 'rev'
444 c.source_ref_type = 'rev'
445 c.source_ref = source_ref_id
445 c.source_ref = source_ref_id
446
446
447 c.target_ref_type = 'rev'
447 c.target_ref_type = 'rev'
448 c.target_ref = target_ref_id
448 c.target_ref = target_ref_id
449
449
450 c.source_repo = source_repo
450 c.source_repo = source_repo
451 c.target_repo = target_repo
451 c.target_repo = target_repo
452
452
453 c.commit_ranges = []
453 c.commit_ranges = []
454 source_commit = EmptyCommit()
454 source_commit = EmptyCommit()
455 target_commit = EmptyCommit()
455 target_commit = EmptyCommit()
456 c.missing_requirements = False
456 c.missing_requirements = False
457
457
458 source_scm = source_repo.scm_instance()
458 source_scm = source_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
460
460
461 # try first shadow repo, fallback to regular repo
461 # try first shadow repo, fallback to regular repo
462 try:
462 try:
463 commits_source_repo = pull_request_latest.get_shadow_repo()
463 commits_source_repo = pull_request_latest.get_shadow_repo()
464 except Exception:
464 except Exception:
465 log.debug('Failed to get shadow repo', exc_info=True)
465 log.debug('Failed to get shadow repo', exc_info=True)
466 commits_source_repo = source_scm
466 commits_source_repo = source_scm
467
467
468 c.commits_source_repo = commits_source_repo
468 c.commits_source_repo = commits_source_repo
469 commit_cache = {}
469 commit_cache = {}
470 try:
470 try:
471 pre_load = ["author", "branch", "date", "message"]
471 pre_load = ["author", "branch", "date", "message"]
472 show_revs = pull_request_at_ver.revisions
472 show_revs = pull_request_at_ver.revisions
473 for rev in show_revs:
473 for rev in show_revs:
474 comm = commits_source_repo.get_commit(
474 comm = commits_source_repo.get_commit(
475 commit_id=rev, pre_load=pre_load)
475 commit_id=rev, pre_load=pre_load)
476 c.commit_ranges.append(comm)
476 c.commit_ranges.append(comm)
477 commit_cache[comm.raw_id] = comm
477 commit_cache[comm.raw_id] = comm
478
478
479 # Order here matters, we first need to get target, and then
479 # Order here matters, we first need to get target, and then
480 # the source
480 # the source
481 target_commit = commits_source_repo.get_commit(
481 target_commit = commits_source_repo.get_commit(
482 commit_id=safe_str(target_ref_id))
482 commit_id=safe_str(target_ref_id))
483
483
484 source_commit = commits_source_repo.get_commit(
484 source_commit = commits_source_repo.get_commit(
485 commit_id=safe_str(source_ref_id))
485 commit_id=safe_str(source_ref_id))
486
486
487 except CommitDoesNotExistError:
487 except CommitDoesNotExistError:
488 log.warning(
488 log.warning(
489 'Failed to get commit from `{}` repo'.format(
489 'Failed to get commit from `{}` repo'.format(
490 commits_source_repo), exc_info=True)
490 commits_source_repo), exc_info=True)
491 except RepositoryRequirementError:
491 except RepositoryRequirementError:
492 log.warning(
492 log.warning(
493 'Failed to get all required data from repo', exc_info=True)
493 'Failed to get all required data from repo', exc_info=True)
494 c.missing_requirements = True
494 c.missing_requirements = True
495
495
496 c.ancestor = None # set it to None, to hide it from PR view
496 c.ancestor = None # set it to None, to hide it from PR view
497
497
498 try:
498 try:
499 ancestor_id = source_scm.get_common_ancestor(
499 ancestor_id = source_scm.get_common_ancestor(
500 source_commit.raw_id, target_commit.raw_id, target_scm)
500 source_commit.raw_id, target_commit.raw_id, target_scm)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
502 except Exception:
502 except Exception:
503 c.ancestor_commit = None
503 c.ancestor_commit = None
504
504
505 c.statuses = source_repo.statuses(
505 c.statuses = source_repo.statuses(
506 [x.raw_id for x in c.commit_ranges])
506 [x.raw_id for x in c.commit_ranges])
507
507
508 # auto collapse if we have more than limit
508 # auto collapse if we have more than limit
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
511 c.compare_mode = compare
511 c.compare_mode = compare
512
512
513 # diff_limit is the old behavior, will cut off the whole diff
513 # diff_limit is the old behavior, will cut off the whole diff
514 # if the limit is applied otherwise will just hide the
514 # if the limit is applied otherwise will just hide the
515 # big files from the front-end
515 # big files from the front-end
516 diff_limit = c.visual.cut_off_limit_diff
516 diff_limit = c.visual.cut_off_limit_diff
517 file_limit = c.visual.cut_off_limit_file
517 file_limit = c.visual.cut_off_limit_file
518
518
519 c.missing_commits = False
519 c.missing_commits = False
520 if (c.missing_requirements
520 if (c.missing_requirements
521 or isinstance(source_commit, EmptyCommit)
521 or isinstance(source_commit, EmptyCommit)
522 or source_commit == target_commit):
522 or source_commit == target_commit):
523
523
524 c.missing_commits = True
524 c.missing_commits = True
525 else:
525 else:
526
526
527 c.diffset = self._get_diffset(
527 c.diffset = self._get_diffset(
528 c.source_repo.repo_name, commits_source_repo,
528 c.source_repo.repo_name, commits_source_repo,
529 source_ref_id, target_ref_id,
529 source_ref_id, target_ref_id,
530 target_commit, source_commit,
530 target_commit, source_commit,
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
532
532
533 c.limited_diff = c.diffset.limited_diff
533 c.limited_diff = c.diffset.limited_diff
534
534
535 # calculate removed files that are bound to comments
535 # calculate removed files that are bound to comments
536 comment_deleted_files = [
536 comment_deleted_files = [
537 fname for fname in display_inline_comments
537 fname for fname in display_inline_comments
538 if fname not in c.diffset.file_stats]
538 if fname not in c.diffset.file_stats]
539
539
540 c.deleted_files_comments = collections.defaultdict(dict)
540 c.deleted_files_comments = collections.defaultdict(dict)
541 for fname, per_line_comments in display_inline_comments.items():
541 for fname, per_line_comments in display_inline_comments.items():
542 if fname in comment_deleted_files:
542 if fname in comment_deleted_files:
543 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['stats'] = 0
544 c.deleted_files_comments[fname]['comments'] = list()
544 c.deleted_files_comments[fname]['comments'] = list()
545 for lno, comments in per_line_comments.items():
545 for lno, comments in per_line_comments.items():
546 c.deleted_files_comments[fname]['comments'].extend(
546 c.deleted_files_comments[fname]['comments'].extend(
547 comments)
547 comments)
548
548
549 # this is a hack to properly display links, when creating PR, the
549 # this is a hack to properly display links, when creating PR, the
550 # compare view and others uses different notation, and
550 # compare view and others uses different notation, and
551 # compare_commits.mako renders links based on the target_repo.
551 # compare_commits.mako renders links based on the target_repo.
552 # We need to swap that here to generate it properly on the html side
552 # We need to swap that here to generate it properly on the html side
553 c.target_repo = c.source_repo
553 c.target_repo = c.source_repo
554
554
555 c.commit_statuses = ChangesetStatus.STATUSES
555 c.commit_statuses = ChangesetStatus.STATUSES
556
556
557 c.show_version_changes = not pr_closed
557 c.show_version_changes = not pr_closed
558 if c.show_version_changes:
558 if c.show_version_changes:
559 cur_obj = pull_request_at_ver
559 cur_obj = pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
561
561
562 old_commit_ids = prev_obj.revisions
562 old_commit_ids = prev_obj.revisions
563 new_commit_ids = cur_obj.revisions
563 new_commit_ids = cur_obj.revisions
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
565 old_commit_ids, new_commit_ids)
565 old_commit_ids, new_commit_ids)
566 c.commit_changes_summary = commit_changes
566 c.commit_changes_summary = commit_changes
567
567
568 # calculate the diff for commits between versions
568 # calculate the diff for commits between versions
569 c.commit_changes = []
569 c.commit_changes = []
570 mark = lambda cs, fw: list(
570 mark = lambda cs, fw: list(
571 h.itertools.izip_longest([], cs, fillvalue=fw))
571 h.itertools.izip_longest([], cs, fillvalue=fw))
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
573 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.removed, 'r') \
574 + mark(commit_changes.common, 'c'):
574 + mark(commit_changes.common, 'c'):
575
575
576 if raw_id in commit_cache:
576 if raw_id in commit_cache:
577 commit = commit_cache[raw_id]
577 commit = commit_cache[raw_id]
578 else:
578 else:
579 try:
579 try:
580 commit = commits_source_repo.get_commit(raw_id)
580 commit = commits_source_repo.get_commit(raw_id)
581 except CommitDoesNotExistError:
581 except CommitDoesNotExistError:
582 # in case we fail extracting still use "dummy" commit
582 # in case we fail extracting still use "dummy" commit
583 # for display in commit diff
583 # for display in commit diff
584 commit = h.AttributeDict(
584 commit = h.AttributeDict(
585 {'raw_id': raw_id,
585 {'raw_id': raw_id,
586 'message': 'EMPTY or MISSING COMMIT'})
586 'message': 'EMPTY or MISSING COMMIT'})
587 c.commit_changes.append([c_type, commit])
587 c.commit_changes.append([c_type, commit])
588
588
589 # current user review statuses for each version
589 # current user review statuses for each version
590 c.review_versions = {}
590 c.review_versions = {}
591 if self._rhodecode_user.user_id in allowed_reviewers:
591 if self._rhodecode_user.user_id in allowed_reviewers:
592 for co in general_comments:
592 for co in general_comments:
593 if co.author.user_id == self._rhodecode_user.user_id:
593 if co.author.user_id == self._rhodecode_user.user_id:
594 # each comment has a status change
594 # each comment has a status change
595 status = co.status_change
595 status = co.status_change
596 if status:
596 if status:
597 _ver_pr = status[0].comment.pull_request_version_id
597 _ver_pr = status[0].comment.pull_request_version_id
598 c.review_versions[_ver_pr] = status[0]
598 c.review_versions[_ver_pr] = status[0]
599
599
600 return self._get_template_context(c)
600 return self._get_template_context(c)
601
601
602 def assure_not_empty_repo(self):
602 def assure_not_empty_repo(self):
603 _ = self.request.translate
603 _ = self.request.translate
604
604
605 try:
605 try:
606 self.db_repo.scm_instance().get_commit()
606 self.db_repo.scm_instance().get_commit()
607 except EmptyRepositoryError:
607 except EmptyRepositoryError:
608 h.flash(h.literal(_('There are no commits yet')),
608 h.flash(h.literal(_('There are no commits yet')),
609 category='warning')
609 category='warning')
610 raise HTTPFound(
610 raise HTTPFound(
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
612
612
613 @LoginRequired()
613 @LoginRequired()
614 @NotAnonymous()
614 @NotAnonymous()
615 @HasRepoPermissionAnyDecorator(
615 @HasRepoPermissionAnyDecorator(
616 'repository.read', 'repository.write', 'repository.admin')
616 'repository.read', 'repository.write', 'repository.admin')
617 @view_config(
617 @view_config(
618 route_name='pullrequest_new', request_method='GET',
618 route_name='pullrequest_new', request_method='GET',
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
620 def pull_request_new(self):
620 def pull_request_new(self):
621 _ = self.request.translate
621 _ = self.request.translate
622 c = self.load_default_context()
622 c = self.load_default_context()
623
623
624 self.assure_not_empty_repo()
624 self.assure_not_empty_repo()
625 source_repo = self.db_repo
625 source_repo = self.db_repo
626
626
627 commit_id = self.request.GET.get('commit')
627 commit_id = self.request.GET.get('commit')
628 branch_ref = self.request.GET.get('branch')
628 branch_ref = self.request.GET.get('branch')
629 bookmark_ref = self.request.GET.get('bookmark')
629 bookmark_ref = self.request.GET.get('bookmark')
630
630
631 try:
631 try:
632 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo_data = PullRequestModel().generate_repo_data(
633 source_repo, commit_id=commit_id,
633 source_repo, commit_id=commit_id,
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
635 except CommitDoesNotExistError as e:
635 except CommitDoesNotExistError as e:
636 log.exception(e)
636 log.exception(e)
637 h.flash(_('Commit does not exist'), 'error')
637 h.flash(_('Commit does not exist'), 'error')
638 raise HTTPFound(
638 raise HTTPFound(
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
640
640
641 default_target_repo = source_repo
641 default_target_repo = source_repo
642
642
643 if source_repo.parent:
643 if source_repo.parent:
644 parent_vcs_obj = source_repo.parent.scm_instance()
644 parent_vcs_obj = source_repo.parent.scm_instance()
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
646 # change default if we have a parent repo
646 # change default if we have a parent repo
647 default_target_repo = source_repo.parent
647 default_target_repo = source_repo.parent
648
648
649 target_repo_data = PullRequestModel().generate_repo_data(
649 target_repo_data = PullRequestModel().generate_repo_data(
650 default_target_repo, translator=self.request.translate)
650 default_target_repo, translator=self.request.translate)
651
651
652 selected_source_ref = source_repo_data['refs']['selected_ref']
652 selected_source_ref = source_repo_data['refs']['selected_ref']
653
653
654 title_source_ref = selected_source_ref.split(':', 2)[1]
654 title_source_ref = selected_source_ref.split(':', 2)[1]
655 c.default_title = PullRequestModel().generate_pullrequest_title(
655 c.default_title = PullRequestModel().generate_pullrequest_title(
656 source=source_repo.repo_name,
656 source=source_repo.repo_name,
657 source_ref=title_source_ref,
657 source_ref=title_source_ref,
658 target=default_target_repo.repo_name
658 target=default_target_repo.repo_name
659 )
659 )
660
660
661 c.default_repo_data = {
661 c.default_repo_data = {
662 'source_repo_name': source_repo.repo_name,
662 'source_repo_name': source_repo.repo_name,
663 'source_refs_json': json.dumps(source_repo_data),
663 'source_refs_json': json.dumps(source_repo_data),
664 'target_repo_name': default_target_repo.repo_name,
664 'target_repo_name': default_target_repo.repo_name,
665 'target_refs_json': json.dumps(target_repo_data),
665 'target_refs_json': json.dumps(target_repo_data),
666 }
666 }
667 c.default_source_ref = selected_source_ref
667 c.default_source_ref = selected_source_ref
668
668
669 return self._get_template_context(c)
669 return self._get_template_context(c)
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator(
673 @HasRepoPermissionAnyDecorator(
674 'repository.read', 'repository.write', 'repository.admin')
674 'repository.read', 'repository.write', 'repository.admin')
675 @view_config(
675 @view_config(
676 route_name='pullrequest_repo_refs', request_method='GET',
676 route_name='pullrequest_repo_refs', request_method='GET',
677 renderer='json_ext', xhr=True)
677 renderer='json_ext', xhr=True)
678 def pull_request_repo_refs(self):
678 def pull_request_repo_refs(self):
679 self.load_default_context()
679 self.load_default_context()
680 target_repo_name = self.request.matchdict['target_repo_name']
680 target_repo_name = self.request.matchdict['target_repo_name']
681 repo = Repository.get_by_repo_name(target_repo_name)
681 repo = Repository.get_by_repo_name(target_repo_name)
682 if not repo:
682 if not repo:
683 raise HTTPNotFound()
683 raise HTTPNotFound()
684
684
685 target_perm = HasRepoPermissionAny(
685 target_perm = HasRepoPermissionAny(
686 'repository.read', 'repository.write', 'repository.admin')(
686 'repository.read', 'repository.write', 'repository.admin')(
687 target_repo_name)
687 target_repo_name)
688 if not target_perm:
688 if not target_perm:
689 raise HTTPNotFound()
689 raise HTTPNotFound()
690
690
691 return PullRequestModel().generate_repo_data(
691 return PullRequestModel().generate_repo_data(
692 repo, translator=self.request.translate)
692 repo, translator=self.request.translate)
693
693
694 @LoginRequired()
694 @LoginRequired()
695 @NotAnonymous()
695 @NotAnonymous()
696 @HasRepoPermissionAnyDecorator(
696 @HasRepoPermissionAnyDecorator(
697 'repository.read', 'repository.write', 'repository.admin')
697 'repository.read', 'repository.write', 'repository.admin')
698 @view_config(
698 @view_config(
699 route_name='pullrequest_repo_destinations', request_method='GET',
699 route_name='pullrequest_repo_destinations', request_method='GET',
700 renderer='json_ext', xhr=True)
700 renderer='json_ext', xhr=True)
701 def pull_request_repo_destinations(self):
701 def pull_request_repo_destinations(self):
702 _ = self.request.translate
702 _ = self.request.translate
703 filter_query = self.request.GET.get('query')
703 filter_query = self.request.GET.get('query')
704
704
705 query = Repository.query() \
705 query = Repository.query() \
706 .order_by(func.length(Repository.repo_name)) \
706 .order_by(func.length(Repository.repo_name)) \
707 .filter(
707 .filter(
708 or_(Repository.repo_name == self.db_repo.repo_name,
708 or_(Repository.repo_name == self.db_repo.repo_name,
709 Repository.fork_id == self.db_repo.repo_id))
709 Repository.fork_id == self.db_repo.repo_id))
710
710
711 if filter_query:
711 if filter_query:
712 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
712 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
713 query = query.filter(
713 query = query.filter(
714 Repository.repo_name.ilike(ilike_expression))
714 Repository.repo_name.ilike(ilike_expression))
715
715
716 add_parent = False
716 add_parent = False
717 if self.db_repo.parent:
717 if self.db_repo.parent:
718 if filter_query in self.db_repo.parent.repo_name:
718 if filter_query in self.db_repo.parent.repo_name:
719 parent_vcs_obj = self.db_repo.parent.scm_instance()
719 parent_vcs_obj = self.db_repo.parent.scm_instance()
720 if parent_vcs_obj and not parent_vcs_obj.is_empty():
720 if parent_vcs_obj and not parent_vcs_obj.is_empty():
721 add_parent = True
721 add_parent = True
722
722
723 limit = 20 - 1 if add_parent else 20
723 limit = 20 - 1 if add_parent else 20
724 all_repos = query.limit(limit).all()
724 all_repos = query.limit(limit).all()
725 if add_parent:
725 if add_parent:
726 all_repos += [self.db_repo.parent]
726 all_repos += [self.db_repo.parent]
727
727
728 repos = []
728 repos = []
729 for obj in ScmModel().get_repos(all_repos):
729 for obj in ScmModel().get_repos(all_repos):
730 repos.append({
730 repos.append({
731 'id': obj['name'],
731 'id': obj['name'],
732 'text': obj['name'],
732 'text': obj['name'],
733 'type': 'repo',
733 'type': 'repo',
734 'obj': obj['dbrepo']
734 'obj': obj['dbrepo']
735 })
735 })
736
736
737 data = {
737 data = {
738 'more': False,
738 'more': False,
739 'results': [{
739 'results': [{
740 'text': _('Repositories'),
740 'text': _('Repositories'),
741 'children': repos
741 'children': repos
742 }] if repos else []
742 }] if repos else []
743 }
743 }
744 return data
744 return data
745
745
746 @LoginRequired()
746 @LoginRequired()
747 @NotAnonymous()
747 @NotAnonymous()
748 @HasRepoPermissionAnyDecorator(
748 @HasRepoPermissionAnyDecorator(
749 'repository.read', 'repository.write', 'repository.admin')
749 'repository.read', 'repository.write', 'repository.admin')
750 @CSRFRequired()
750 @CSRFRequired()
751 @view_config(
751 @view_config(
752 route_name='pullrequest_create', request_method='POST',
752 route_name='pullrequest_create', request_method='POST',
753 renderer=None)
753 renderer=None)
754 def pull_request_create(self):
754 def pull_request_create(self):
755 _ = self.request.translate
755 _ = self.request.translate
756 self.assure_not_empty_repo()
756 self.assure_not_empty_repo()
757 self.load_default_context()
757 self.load_default_context()
758
758
759 controls = peppercorn.parse(self.request.POST.items())
759 controls = peppercorn.parse(self.request.POST.items())
760
760
761 try:
761 try:
762 form = PullRequestForm(
762 form = PullRequestForm(
763 self.request.translate, self.db_repo.repo_id)()
763 self.request.translate, self.db_repo.repo_id)()
764 _form = form.to_python(controls)
764 _form = form.to_python(controls)
765 except formencode.Invalid as errors:
765 except formencode.Invalid as errors:
766 if errors.error_dict.get('revisions'):
766 if errors.error_dict.get('revisions'):
767 msg = 'Revisions: %s' % errors.error_dict['revisions']
767 msg = 'Revisions: %s' % errors.error_dict['revisions']
768 elif errors.error_dict.get('pullrequest_title'):
768 elif errors.error_dict.get('pullrequest_title'):
769 msg = _('Pull request requires a title with min. 3 chars')
769 msg = _('Pull request requires a title with min. 3 chars')
770 else:
770 else:
771 msg = _('Error creating pull request: {}').format(errors)
771 msg = _('Error creating pull request: {}').format(errors)
772 log.exception(msg)
772 log.exception(msg)
773 h.flash(msg, 'error')
773 h.flash(msg, 'error')
774
774
775 # would rather just go back to form ...
775 # would rather just go back to form ...
776 raise HTTPFound(
776 raise HTTPFound(
777 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
777 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
778
778
779 source_repo = _form['source_repo']
779 source_repo = _form['source_repo']
780 source_ref = _form['source_ref']
780 source_ref = _form['source_ref']
781 target_repo = _form['target_repo']
781 target_repo = _form['target_repo']
782 target_ref = _form['target_ref']
782 target_ref = _form['target_ref']
783 commit_ids = _form['revisions'][::-1]
783 commit_ids = _form['revisions'][::-1]
784
784
785 # find the ancestor for this pr
785 # find the ancestor for this pr
786 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
786 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
787 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
787 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
788
788
789 # re-check permissions again here
789 # re-check permissions again here
790 # source_repo we must have read permissions
790 # source_repo we must have read permissions
791
791
792 source_perm = HasRepoPermissionAny(
792 source_perm = HasRepoPermissionAny(
793 'repository.read',
793 'repository.read',
794 'repository.write', 'repository.admin')(source_db_repo.repo_name)
794 'repository.write', 'repository.admin')(source_db_repo.repo_name)
795 if not source_perm:
795 if not source_perm:
796 msg = _('Not Enough permissions to source repo `{}`.'.format(
796 msg = _('Not Enough permissions to source repo `{}`.'.format(
797 source_db_repo.repo_name))
797 source_db_repo.repo_name))
798 h.flash(msg, category='error')
798 h.flash(msg, category='error')
799 # copy the args back to redirect
799 # copy the args back to redirect
800 org_query = self.request.GET.mixed()
800 org_query = self.request.GET.mixed()
801 raise HTTPFound(
801 raise HTTPFound(
802 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
802 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
803 _query=org_query))
803 _query=org_query))
804
804
805 # target repo we must have read permissions, and also later on
805 # target repo we must have read permissions, and also later on
806 # we want to check branch permissions here
806 # we want to check branch permissions here
807 target_perm = HasRepoPermissionAny(
807 target_perm = HasRepoPermissionAny(
808 'repository.read',
808 'repository.read',
809 'repository.write', 'repository.admin')(target_db_repo.repo_name)
809 'repository.write', 'repository.admin')(target_db_repo.repo_name)
810 if not target_perm:
810 if not target_perm:
811 msg = _('Not Enough permissions to target repo `{}`.'.format(
811 msg = _('Not Enough permissions to target repo `{}`.'.format(
812 target_db_repo.repo_name))
812 target_db_repo.repo_name))
813 h.flash(msg, category='error')
813 h.flash(msg, category='error')
814 # copy the args back to redirect
814 # copy the args back to redirect
815 org_query = self.request.GET.mixed()
815 org_query = self.request.GET.mixed()
816 raise HTTPFound(
816 raise HTTPFound(
817 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
817 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
818 _query=org_query))
818 _query=org_query))
819
819
820 source_scm = source_db_repo.scm_instance()
820 source_scm = source_db_repo.scm_instance()
821 target_scm = target_db_repo.scm_instance()
821 target_scm = target_db_repo.scm_instance()
822
822
823 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
823 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
824 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
824 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
825
825
826 ancestor = source_scm.get_common_ancestor(
826 ancestor = source_scm.get_common_ancestor(
827 source_commit.raw_id, target_commit.raw_id, target_scm)
827 source_commit.raw_id, target_commit.raw_id, target_scm)
828
828
829 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
829 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
830 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
830 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
831
831
832 pullrequest_title = _form['pullrequest_title']
832 pullrequest_title = _form['pullrequest_title']
833 title_source_ref = source_ref.split(':', 2)[1]
833 title_source_ref = source_ref.split(':', 2)[1]
834 if not pullrequest_title:
834 if not pullrequest_title:
835 pullrequest_title = PullRequestModel().generate_pullrequest_title(
835 pullrequest_title = PullRequestModel().generate_pullrequest_title(
836 source=source_repo,
836 source=source_repo,
837 source_ref=title_source_ref,
837 source_ref=title_source_ref,
838 target=target_repo
838 target=target_repo
839 )
839 )
840
840
841 description = _form['pullrequest_desc']
841 description = _form['pullrequest_desc']
842
842
843 get_default_reviewers_data, validate_default_reviewers = \
843 get_default_reviewers_data, validate_default_reviewers = \
844 PullRequestModel().get_reviewer_functions()
844 PullRequestModel().get_reviewer_functions()
845
845
846 # recalculate reviewers logic, to make sure we can validate this
846 # recalculate reviewers logic, to make sure we can validate this
847 reviewer_rules = get_default_reviewers_data(
847 reviewer_rules = get_default_reviewers_data(
848 self._rhodecode_db_user, source_db_repo,
848 self._rhodecode_db_user, source_db_repo,
849 source_commit, target_db_repo, target_commit)
849 source_commit, target_db_repo, target_commit)
850
850
851 given_reviewers = _form['review_members']
851 given_reviewers = _form['review_members']
852 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
852 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
853
853
854 try:
854 try:
855 pull_request = PullRequestModel().create(
855 pull_request = PullRequestModel().create(
856 self._rhodecode_user.user_id, source_repo, source_ref,
856 self._rhodecode_user.user_id, source_repo, source_ref,
857 target_repo, target_ref, commit_ids, reviewers,
857 target_repo, target_ref, commit_ids, reviewers,
858 pullrequest_title, description, reviewer_rules
858 pullrequest_title, description, reviewer_rules
859 )
859 )
860 Session().commit()
860 Session().commit()
861
861
862 h.flash(_('Successfully opened new pull request'),
862 h.flash(_('Successfully opened new pull request'),
863 category='success')
863 category='success')
864 except Exception:
864 except Exception:
865 msg = _('Error occurred during creation of this pull request.')
865 msg = _('Error occurred during creation of this pull request.')
866 log.exception(msg)
866 log.exception(msg)
867 h.flash(msg, category='error')
867 h.flash(msg, category='error')
868
868
869 # copy the args back to redirect
869 # copy the args back to redirect
870 org_query = self.request.GET.mixed()
870 org_query = self.request.GET.mixed()
871 raise HTTPFound(
871 raise HTTPFound(
872 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
872 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
873 _query=org_query))
873 _query=org_query))
874
874
875 raise HTTPFound(
875 raise HTTPFound(
876 h.route_path('pullrequest_show', repo_name=target_repo,
876 h.route_path('pullrequest_show', repo_name=target_repo,
877 pull_request_id=pull_request.pull_request_id))
877 pull_request_id=pull_request.pull_request_id))
878
878
879 @LoginRequired()
879 @LoginRequired()
880 @NotAnonymous()
880 @NotAnonymous()
881 @HasRepoPermissionAnyDecorator(
881 @HasRepoPermissionAnyDecorator(
882 'repository.read', 'repository.write', 'repository.admin')
882 'repository.read', 'repository.write', 'repository.admin')
883 @CSRFRequired()
883 @CSRFRequired()
884 @view_config(
884 @view_config(
885 route_name='pullrequest_update', request_method='POST',
885 route_name='pullrequest_update', request_method='POST',
886 renderer='json_ext')
886 renderer='json_ext')
887 def pull_request_update(self):
887 def pull_request_update(self):
888 pull_request = PullRequest.get_or_404(
888 pull_request = PullRequest.get_or_404(
889 self.request.matchdict['pull_request_id'])
889 self.request.matchdict['pull_request_id'])
890 _ = self.request.translate
890
891
891 self.load_default_context()
892 self.load_default_context()
893
894 if pull_request.is_closed():
895 log.debug('update: forbidden because pull request is closed')
896 msg = _(u'Cannot update closed pull requests.')
897 h.flash(msg, category='error')
898 return True
899
892 # only owner or admin can update it
900 # only owner or admin can update it
893 allowed_to_update = PullRequestModel().check_user_update(
901 allowed_to_update = PullRequestModel().check_user_update(
894 pull_request, self._rhodecode_user)
902 pull_request, self._rhodecode_user)
895 if allowed_to_update:
903 if allowed_to_update:
896 controls = peppercorn.parse(self.request.POST.items())
904 controls = peppercorn.parse(self.request.POST.items())
897
905
898 if 'review_members' in controls:
906 if 'review_members' in controls:
899 self._update_reviewers(
907 self._update_reviewers(
900 pull_request, controls['review_members'],
908 pull_request, controls['review_members'],
901 pull_request.reviewer_data)
909 pull_request.reviewer_data)
902 elif str2bool(self.request.POST.get('update_commits', 'false')):
910 elif str2bool(self.request.POST.get('update_commits', 'false')):
903 self._update_commits(pull_request)
911 self._update_commits(pull_request)
904 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
912 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
905 self._edit_pull_request(pull_request)
913 self._edit_pull_request(pull_request)
906 else:
914 else:
907 raise HTTPBadRequest()
915 raise HTTPBadRequest()
908 return True
916 return True
909 raise HTTPForbidden()
917 raise HTTPForbidden()
910
918
911 def _edit_pull_request(self, pull_request):
919 def _edit_pull_request(self, pull_request):
912 _ = self.request.translate
920 _ = self.request.translate
913 try:
921 try:
914 PullRequestModel().edit(
922 PullRequestModel().edit(
915 pull_request, self.request.POST.get('title'),
923 pull_request, self.request.POST.get('title'),
916 self.request.POST.get('description'), self._rhodecode_user)
924 self.request.POST.get('description'), self._rhodecode_user)
917 except ValueError:
925 except ValueError:
918 msg = _(u'Cannot update closed pull requests.')
926 msg = _(u'Cannot update closed pull requests.')
919 h.flash(msg, category='error')
927 h.flash(msg, category='error')
920 return
928 return
921 else:
929 else:
922 Session().commit()
930 Session().commit()
923
931
924 msg = _(u'Pull request title & description updated.')
932 msg = _(u'Pull request title & description updated.')
925 h.flash(msg, category='success')
933 h.flash(msg, category='success')
926 return
934 return
927
935
928 def _update_commits(self, pull_request):
936 def _update_commits(self, pull_request):
929 _ = self.request.translate
937 _ = self.request.translate
930 resp = PullRequestModel().update_commits(pull_request)
938 resp = PullRequestModel().update_commits(pull_request)
931
939
932 if resp.executed:
940 if resp.executed:
933
941
934 if resp.target_changed and resp.source_changed:
942 if resp.target_changed and resp.source_changed:
935 changed = 'target and source repositories'
943 changed = 'target and source repositories'
936 elif resp.target_changed and not resp.source_changed:
944 elif resp.target_changed and not resp.source_changed:
937 changed = 'target repository'
945 changed = 'target repository'
938 elif not resp.target_changed and resp.source_changed:
946 elif not resp.target_changed and resp.source_changed:
939 changed = 'source repository'
947 changed = 'source repository'
940 else:
948 else:
941 changed = 'nothing'
949 changed = 'nothing'
942
950
943 msg = _(
951 msg = _(
944 u'Pull request updated to "{source_commit_id}" with '
952 u'Pull request updated to "{source_commit_id}" with '
945 u'{count_added} added, {count_removed} removed commits. '
953 u'{count_added} added, {count_removed} removed commits. '
946 u'Source of changes: {change_source}')
954 u'Source of changes: {change_source}')
947 msg = msg.format(
955 msg = msg.format(
948 source_commit_id=pull_request.source_ref_parts.commit_id,
956 source_commit_id=pull_request.source_ref_parts.commit_id,
949 count_added=len(resp.changes.added),
957 count_added=len(resp.changes.added),
950 count_removed=len(resp.changes.removed),
958 count_removed=len(resp.changes.removed),
951 change_source=changed)
959 change_source=changed)
952 h.flash(msg, category='success')
960 h.flash(msg, category='success')
953
961
954 channel = '/repo${}$/pr/{}'.format(
962 channel = '/repo${}$/pr/{}'.format(
955 pull_request.target_repo.repo_name,
963 pull_request.target_repo.repo_name,
956 pull_request.pull_request_id)
964 pull_request.pull_request_id)
957 message = msg + (
965 message = msg + (
958 ' - <a onclick="window.location.reload()">'
966 ' - <a onclick="window.location.reload()">'
959 '<strong>{}</strong></a>'.format(_('Reload page')))
967 '<strong>{}</strong></a>'.format(_('Reload page')))
960 channelstream.post_message(
968 channelstream.post_message(
961 channel, message, self._rhodecode_user.username,
969 channel, message, self._rhodecode_user.username,
962 registry=self.request.registry)
970 registry=self.request.registry)
963 else:
971 else:
964 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
972 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
965 warning_reasons = [
973 warning_reasons = [
966 UpdateFailureReason.NO_CHANGE,
974 UpdateFailureReason.NO_CHANGE,
967 UpdateFailureReason.WRONG_REF_TYPE,
975 UpdateFailureReason.WRONG_REF_TYPE,
968 ]
976 ]
969 category = 'warning' if resp.reason in warning_reasons else 'error'
977 category = 'warning' if resp.reason in warning_reasons else 'error'
970 h.flash(msg, category=category)
978 h.flash(msg, category=category)
971
979
972 @LoginRequired()
980 @LoginRequired()
973 @NotAnonymous()
981 @NotAnonymous()
974 @HasRepoPermissionAnyDecorator(
982 @HasRepoPermissionAnyDecorator(
975 'repository.read', 'repository.write', 'repository.admin')
983 'repository.read', 'repository.write', 'repository.admin')
976 @CSRFRequired()
984 @CSRFRequired()
977 @view_config(
985 @view_config(
978 route_name='pullrequest_merge', request_method='POST',
986 route_name='pullrequest_merge', request_method='POST',
979 renderer='json_ext')
987 renderer='json_ext')
980 def pull_request_merge(self):
988 def pull_request_merge(self):
981 """
989 """
982 Merge will perform a server-side merge of the specified
990 Merge will perform a server-side merge of the specified
983 pull request, if the pull request is approved and mergeable.
991 pull request, if the pull request is approved and mergeable.
984 After successful merging, the pull request is automatically
992 After successful merging, the pull request is automatically
985 closed, with a relevant comment.
993 closed, with a relevant comment.
986 """
994 """
987 pull_request = PullRequest.get_or_404(
995 pull_request = PullRequest.get_or_404(
988 self.request.matchdict['pull_request_id'])
996 self.request.matchdict['pull_request_id'])
989
997
990 self.load_default_context()
998 self.load_default_context()
991 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
999 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
992 translator=self.request.translate)
1000 translator=self.request.translate)
993 merge_possible = not check.failed
1001 merge_possible = not check.failed
994
1002
995 for err_type, error_msg in check.errors:
1003 for err_type, error_msg in check.errors:
996 h.flash(error_msg, category=err_type)
1004 h.flash(error_msg, category=err_type)
997
1005
998 if merge_possible:
1006 if merge_possible:
999 log.debug("Pre-conditions checked, trying to merge.")
1007 log.debug("Pre-conditions checked, trying to merge.")
1000 extras = vcs_operation_context(
1008 extras = vcs_operation_context(
1001 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1009 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1002 username=self._rhodecode_db_user.username, action='push',
1010 username=self._rhodecode_db_user.username, action='push',
1003 scm=pull_request.target_repo.repo_type)
1011 scm=pull_request.target_repo.repo_type)
1004 self._merge_pull_request(
1012 self._merge_pull_request(
1005 pull_request, self._rhodecode_db_user, extras)
1013 pull_request, self._rhodecode_db_user, extras)
1006 else:
1014 else:
1007 log.debug("Pre-conditions failed, NOT merging.")
1015 log.debug("Pre-conditions failed, NOT merging.")
1008
1016
1009 raise HTTPFound(
1017 raise HTTPFound(
1010 h.route_path('pullrequest_show',
1018 h.route_path('pullrequest_show',
1011 repo_name=pull_request.target_repo.repo_name,
1019 repo_name=pull_request.target_repo.repo_name,
1012 pull_request_id=pull_request.pull_request_id))
1020 pull_request_id=pull_request.pull_request_id))
1013
1021
1014 def _merge_pull_request(self, pull_request, user, extras):
1022 def _merge_pull_request(self, pull_request, user, extras):
1015 _ = self.request.translate
1023 _ = self.request.translate
1016 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1024 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1017
1025
1018 if merge_resp.executed:
1026 if merge_resp.executed:
1019 log.debug("The merge was successful, closing the pull request.")
1027 log.debug("The merge was successful, closing the pull request.")
1020 PullRequestModel().close_pull_request(
1028 PullRequestModel().close_pull_request(
1021 pull_request.pull_request_id, user)
1029 pull_request.pull_request_id, user)
1022 Session().commit()
1030 Session().commit()
1023 msg = _('Pull request was successfully merged and closed.')
1031 msg = _('Pull request was successfully merged and closed.')
1024 h.flash(msg, category='success')
1032 h.flash(msg, category='success')
1025 else:
1033 else:
1026 log.debug(
1034 log.debug(
1027 "The merge was not successful. Merge response: %s",
1035 "The merge was not successful. Merge response: %s",
1028 merge_resp)
1036 merge_resp)
1029 msg = PullRequestModel().merge_status_message(
1037 msg = PullRequestModel().merge_status_message(
1030 merge_resp.failure_reason)
1038 merge_resp.failure_reason)
1031 h.flash(msg, category='error')
1039 h.flash(msg, category='error')
1032
1040
1033 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1041 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1034 _ = self.request.translate
1042 _ = self.request.translate
1035 get_default_reviewers_data, validate_default_reviewers = \
1043 get_default_reviewers_data, validate_default_reviewers = \
1036 PullRequestModel().get_reviewer_functions()
1044 PullRequestModel().get_reviewer_functions()
1037
1045
1038 try:
1046 try:
1039 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1047 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1040 except ValueError as e:
1048 except ValueError as e:
1041 log.error('Reviewers Validation: {}'.format(e))
1049 log.error('Reviewers Validation: {}'.format(e))
1042 h.flash(e, category='error')
1050 h.flash(e, category='error')
1043 return
1051 return
1044
1052
1045 PullRequestModel().update_reviewers(
1053 PullRequestModel().update_reviewers(
1046 pull_request, reviewers, self._rhodecode_user)
1054 pull_request, reviewers, self._rhodecode_user)
1047 h.flash(_('Pull request reviewers updated.'), category='success')
1055 h.flash(_('Pull request reviewers updated.'), category='success')
1048 Session().commit()
1056 Session().commit()
1049
1057
1050 @LoginRequired()
1058 @LoginRequired()
1051 @NotAnonymous()
1059 @NotAnonymous()
1052 @HasRepoPermissionAnyDecorator(
1060 @HasRepoPermissionAnyDecorator(
1053 'repository.read', 'repository.write', 'repository.admin')
1061 'repository.read', 'repository.write', 'repository.admin')
1054 @CSRFRequired()
1062 @CSRFRequired()
1055 @view_config(
1063 @view_config(
1056 route_name='pullrequest_delete', request_method='POST',
1064 route_name='pullrequest_delete', request_method='POST',
1057 renderer='json_ext')
1065 renderer='json_ext')
1058 def pull_request_delete(self):
1066 def pull_request_delete(self):
1059 _ = self.request.translate
1067 _ = self.request.translate
1060
1068
1061 pull_request = PullRequest.get_or_404(
1069 pull_request = PullRequest.get_or_404(
1062 self.request.matchdict['pull_request_id'])
1070 self.request.matchdict['pull_request_id'])
1063 self.load_default_context()
1071 self.load_default_context()
1064
1072
1065 pr_closed = pull_request.is_closed()
1073 pr_closed = pull_request.is_closed()
1066 allowed_to_delete = PullRequestModel().check_user_delete(
1074 allowed_to_delete = PullRequestModel().check_user_delete(
1067 pull_request, self._rhodecode_user) and not pr_closed
1075 pull_request, self._rhodecode_user) and not pr_closed
1068
1076
1069 # only owner can delete it !
1077 # only owner can delete it !
1070 if allowed_to_delete:
1078 if allowed_to_delete:
1071 PullRequestModel().delete(pull_request, self._rhodecode_user)
1079 PullRequestModel().delete(pull_request, self._rhodecode_user)
1072 Session().commit()
1080 Session().commit()
1073 h.flash(_('Successfully deleted pull request'),
1081 h.flash(_('Successfully deleted pull request'),
1074 category='success')
1082 category='success')
1075 raise HTTPFound(h.route_path('pullrequest_show_all',
1083 raise HTTPFound(h.route_path('pullrequest_show_all',
1076 repo_name=self.db_repo_name))
1084 repo_name=self.db_repo_name))
1077
1085
1078 log.warning('user %s tried to delete pull request without access',
1086 log.warning('user %s tried to delete pull request without access',
1079 self._rhodecode_user)
1087 self._rhodecode_user)
1080 raise HTTPNotFound()
1088 raise HTTPNotFound()
1081
1089
1082 @LoginRequired()
1090 @LoginRequired()
1083 @NotAnonymous()
1091 @NotAnonymous()
1084 @HasRepoPermissionAnyDecorator(
1092 @HasRepoPermissionAnyDecorator(
1085 'repository.read', 'repository.write', 'repository.admin')
1093 'repository.read', 'repository.write', 'repository.admin')
1086 @CSRFRequired()
1094 @CSRFRequired()
1087 @view_config(
1095 @view_config(
1088 route_name='pullrequest_comment_create', request_method='POST',
1096 route_name='pullrequest_comment_create', request_method='POST',
1089 renderer='json_ext')
1097 renderer='json_ext')
1090 def pull_request_comment_create(self):
1098 def pull_request_comment_create(self):
1091 _ = self.request.translate
1099 _ = self.request.translate
1092
1100
1093 pull_request = PullRequest.get_or_404(
1101 pull_request = PullRequest.get_or_404(
1094 self.request.matchdict['pull_request_id'])
1102 self.request.matchdict['pull_request_id'])
1095 pull_request_id = pull_request.pull_request_id
1103 pull_request_id = pull_request.pull_request_id
1096
1104
1097 if pull_request.is_closed():
1105 if pull_request.is_closed():
1098 log.debug('comment: forbidden because pull request is closed')
1106 log.debug('comment: forbidden because pull request is closed')
1099 raise HTTPForbidden()
1107 raise HTTPForbidden()
1100
1108
1101 allowed_to_comment = PullRequestModel().check_user_comment(
1109 allowed_to_comment = PullRequestModel().check_user_comment(
1102 pull_request, self._rhodecode_user)
1110 pull_request, self._rhodecode_user)
1103 if not allowed_to_comment:
1111 if not allowed_to_comment:
1104 log.debug(
1112 log.debug(
1105 'comment: forbidden because pull request is from forbidden repo')
1113 'comment: forbidden because pull request is from forbidden repo')
1106 raise HTTPForbidden()
1114 raise HTTPForbidden()
1107
1115
1108 c = self.load_default_context()
1116 c = self.load_default_context()
1109
1117
1110 status = self.request.POST.get('changeset_status', None)
1118 status = self.request.POST.get('changeset_status', None)
1111 text = self.request.POST.get('text')
1119 text = self.request.POST.get('text')
1112 comment_type = self.request.POST.get('comment_type')
1120 comment_type = self.request.POST.get('comment_type')
1113 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1121 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1114 close_pull_request = self.request.POST.get('close_pull_request')
1122 close_pull_request = self.request.POST.get('close_pull_request')
1115
1123
1116 # the logic here should work like following, if we submit close
1124 # the logic here should work like following, if we submit close
1117 # pr comment, use `close_pull_request_with_comment` function
1125 # pr comment, use `close_pull_request_with_comment` function
1118 # else handle regular comment logic
1126 # else handle regular comment logic
1119
1127
1120 if close_pull_request:
1128 if close_pull_request:
1121 # only owner or admin or person with write permissions
1129 # only owner or admin or person with write permissions
1122 allowed_to_close = PullRequestModel().check_user_update(
1130 allowed_to_close = PullRequestModel().check_user_update(
1123 pull_request, self._rhodecode_user)
1131 pull_request, self._rhodecode_user)
1124 if not allowed_to_close:
1132 if not allowed_to_close:
1125 log.debug('comment: forbidden because not allowed to close '
1133 log.debug('comment: forbidden because not allowed to close '
1126 'pull request %s', pull_request_id)
1134 'pull request %s', pull_request_id)
1127 raise HTTPForbidden()
1135 raise HTTPForbidden()
1128 comment, status = PullRequestModel().close_pull_request_with_comment(
1136 comment, status = PullRequestModel().close_pull_request_with_comment(
1129 pull_request, self._rhodecode_user, self.db_repo, message=text)
1137 pull_request, self._rhodecode_user, self.db_repo, message=text)
1130 Session().flush()
1138 Session().flush()
1131 events.trigger(
1139 events.trigger(
1132 events.PullRequestCommentEvent(pull_request, comment))
1140 events.PullRequestCommentEvent(pull_request, comment))
1133
1141
1134 else:
1142 else:
1135 # regular comment case, could be inline, or one with status.
1143 # regular comment case, could be inline, or one with status.
1136 # for that one we check also permissions
1144 # for that one we check also permissions
1137
1145
1138 allowed_to_change_status = PullRequestModel().check_user_change_status(
1146 allowed_to_change_status = PullRequestModel().check_user_change_status(
1139 pull_request, self._rhodecode_user)
1147 pull_request, self._rhodecode_user)
1140
1148
1141 if status and allowed_to_change_status:
1149 if status and allowed_to_change_status:
1142 message = (_('Status change %(transition_icon)s %(status)s')
1150 message = (_('Status change %(transition_icon)s %(status)s')
1143 % {'transition_icon': '>',
1151 % {'transition_icon': '>',
1144 'status': ChangesetStatus.get_status_lbl(status)})
1152 'status': ChangesetStatus.get_status_lbl(status)})
1145 text = text or message
1153 text = text or message
1146
1154
1147 comment = CommentsModel().create(
1155 comment = CommentsModel().create(
1148 text=text,
1156 text=text,
1149 repo=self.db_repo.repo_id,
1157 repo=self.db_repo.repo_id,
1150 user=self._rhodecode_user.user_id,
1158 user=self._rhodecode_user.user_id,
1151 pull_request=pull_request,
1159 pull_request=pull_request,
1152 f_path=self.request.POST.get('f_path'),
1160 f_path=self.request.POST.get('f_path'),
1153 line_no=self.request.POST.get('line'),
1161 line_no=self.request.POST.get('line'),
1154 status_change=(ChangesetStatus.get_status_lbl(status)
1162 status_change=(ChangesetStatus.get_status_lbl(status)
1155 if status and allowed_to_change_status else None),
1163 if status and allowed_to_change_status else None),
1156 status_change_type=(status
1164 status_change_type=(status
1157 if status and allowed_to_change_status else None),
1165 if status and allowed_to_change_status else None),
1158 comment_type=comment_type,
1166 comment_type=comment_type,
1159 resolves_comment_id=resolves_comment_id
1167 resolves_comment_id=resolves_comment_id
1160 )
1168 )
1161
1169
1162 if allowed_to_change_status:
1170 if allowed_to_change_status:
1163 # calculate old status before we change it
1171 # calculate old status before we change it
1164 old_calculated_status = pull_request.calculated_review_status()
1172 old_calculated_status = pull_request.calculated_review_status()
1165
1173
1166 # get status if set !
1174 # get status if set !
1167 if status:
1175 if status:
1168 ChangesetStatusModel().set_status(
1176 ChangesetStatusModel().set_status(
1169 self.db_repo.repo_id,
1177 self.db_repo.repo_id,
1170 status,
1178 status,
1171 self._rhodecode_user.user_id,
1179 self._rhodecode_user.user_id,
1172 comment,
1180 comment,
1173 pull_request=pull_request
1181 pull_request=pull_request
1174 )
1182 )
1175
1183
1176 Session().flush()
1184 Session().flush()
1177 events.trigger(
1185 events.trigger(
1178 events.PullRequestCommentEvent(pull_request, comment))
1186 events.PullRequestCommentEvent(pull_request, comment))
1179
1187
1180 # we now calculate the status of pull request, and based on that
1188 # we now calculate the status of pull request, and based on that
1181 # calculation we set the commits status
1189 # calculation we set the commits status
1182 calculated_status = pull_request.calculated_review_status()
1190 calculated_status = pull_request.calculated_review_status()
1183 if old_calculated_status != calculated_status:
1191 if old_calculated_status != calculated_status:
1184 PullRequestModel()._trigger_pull_request_hook(
1192 PullRequestModel()._trigger_pull_request_hook(
1185 pull_request, self._rhodecode_user, 'review_status_change')
1193 pull_request, self._rhodecode_user, 'review_status_change')
1186
1194
1187 Session().commit()
1195 Session().commit()
1188
1196
1189 data = {
1197 data = {
1190 'target_id': h.safeid(h.safe_unicode(
1198 'target_id': h.safeid(h.safe_unicode(
1191 self.request.POST.get('f_path'))),
1199 self.request.POST.get('f_path'))),
1192 }
1200 }
1193 if comment:
1201 if comment:
1194 c.co = comment
1202 c.co = comment
1195 rendered_comment = render(
1203 rendered_comment = render(
1196 'rhodecode:templates/changeset/changeset_comment_block.mako',
1204 'rhodecode:templates/changeset/changeset_comment_block.mako',
1197 self._get_template_context(c), self.request)
1205 self._get_template_context(c), self.request)
1198
1206
1199 data.update(comment.get_dict())
1207 data.update(comment.get_dict())
1200 data.update({'rendered_text': rendered_comment})
1208 data.update({'rendered_text': rendered_comment})
1201
1209
1202 return data
1210 return data
1203
1211
1204 @LoginRequired()
1212 @LoginRequired()
1205 @NotAnonymous()
1213 @NotAnonymous()
1206 @HasRepoPermissionAnyDecorator(
1214 @HasRepoPermissionAnyDecorator(
1207 'repository.read', 'repository.write', 'repository.admin')
1215 'repository.read', 'repository.write', 'repository.admin')
1208 @CSRFRequired()
1216 @CSRFRequired()
1209 @view_config(
1217 @view_config(
1210 route_name='pullrequest_comment_delete', request_method='POST',
1218 route_name='pullrequest_comment_delete', request_method='POST',
1211 renderer='json_ext')
1219 renderer='json_ext')
1212 def pull_request_comment_delete(self):
1220 def pull_request_comment_delete(self):
1213 pull_request = PullRequest.get_or_404(
1221 pull_request = PullRequest.get_or_404(
1214 self.request.matchdict['pull_request_id'])
1222 self.request.matchdict['pull_request_id'])
1215
1223
1216 comment = ChangesetComment.get_or_404(
1224 comment = ChangesetComment.get_or_404(
1217 self.request.matchdict['comment_id'])
1225 self.request.matchdict['comment_id'])
1218 comment_id = comment.comment_id
1226 comment_id = comment.comment_id
1219
1227
1220 if pull_request.is_closed():
1228 if pull_request.is_closed():
1221 log.debug('comment: forbidden because pull request is closed')
1229 log.debug('comment: forbidden because pull request is closed')
1222 raise HTTPForbidden()
1230 raise HTTPForbidden()
1223
1231
1224 if not comment:
1232 if not comment:
1225 log.debug('Comment with id:%s not found, skipping', comment_id)
1233 log.debug('Comment with id:%s not found, skipping', comment_id)
1226 # comment already deleted in another call probably
1234 # comment already deleted in another call probably
1227 return True
1235 return True
1228
1236
1229 if comment.pull_request.is_closed():
1237 if comment.pull_request.is_closed():
1230 # don't allow deleting comments on closed pull request
1238 # don't allow deleting comments on closed pull request
1231 raise HTTPForbidden()
1239 raise HTTPForbidden()
1232
1240
1233 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1241 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1234 super_admin = h.HasPermissionAny('hg.admin')()
1242 super_admin = h.HasPermissionAny('hg.admin')()
1235 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1243 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1236 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1244 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1237 comment_repo_admin = is_repo_admin and is_repo_comment
1245 comment_repo_admin = is_repo_admin and is_repo_comment
1238
1246
1239 if super_admin or comment_owner or comment_repo_admin:
1247 if super_admin or comment_owner or comment_repo_admin:
1240 old_calculated_status = comment.pull_request.calculated_review_status()
1248 old_calculated_status = comment.pull_request.calculated_review_status()
1241 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1249 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1242 Session().commit()
1250 Session().commit()
1243 calculated_status = comment.pull_request.calculated_review_status()
1251 calculated_status = comment.pull_request.calculated_review_status()
1244 if old_calculated_status != calculated_status:
1252 if old_calculated_status != calculated_status:
1245 PullRequestModel()._trigger_pull_request_hook(
1253 PullRequestModel()._trigger_pull_request_hook(
1246 comment.pull_request, self._rhodecode_user, 'review_status_change')
1254 comment.pull_request, self._rhodecode_user, 'review_status_change')
1247 return True
1255 return True
1248 else:
1256 else:
1249 log.warning('No permissions for user %s to delete comment_id: %s',
1257 log.warning('No permissions for user %s to delete comment_id: %s',
1250 self._rhodecode_db_user, comment_id)
1258 self._rhodecode_db_user, comment_id)
1251 raise HTTPNotFound()
1259 raise HTTPNotFound()
@@ -1,1626 +1,1628 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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)
54 PullRequestVersion, ChangesetComment, Repository)
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 create(self, created_by, source_repo, source_ref, target_repo,
422 def create(self, created_by, source_repo, source_ref, target_repo,
423 target_ref, revisions, reviewers, title, description=None,
423 target_ref, revisions, reviewers, title, description=None,
424 reviewer_data=None, translator=None):
424 reviewer_data=None, translator=None):
425 translator = translator or get_current_request().translate
425 translator = translator or get_current_request().translate
426
426
427 created_by_user = self._get_user(created_by)
427 created_by_user = self._get_user(created_by)
428 source_repo = self._get_repo(source_repo)
428 source_repo = self._get_repo(source_repo)
429 target_repo = self._get_repo(target_repo)
429 target_repo = self._get_repo(target_repo)
430
430
431 pull_request = PullRequest()
431 pull_request = PullRequest()
432 pull_request.source_repo = source_repo
432 pull_request.source_repo = source_repo
433 pull_request.source_ref = source_ref
433 pull_request.source_ref = source_ref
434 pull_request.target_repo = target_repo
434 pull_request.target_repo = target_repo
435 pull_request.target_ref = target_ref
435 pull_request.target_ref = target_ref
436 pull_request.revisions = revisions
436 pull_request.revisions = revisions
437 pull_request.title = title
437 pull_request.title = title
438 pull_request.description = description
438 pull_request.description = description
439 pull_request.author = created_by_user
439 pull_request.author = created_by_user
440 pull_request.reviewer_data = reviewer_data
440 pull_request.reviewer_data = reviewer_data
441
441
442 Session().add(pull_request)
442 Session().add(pull_request)
443 Session().flush()
443 Session().flush()
444
444
445 reviewer_ids = set()
445 reviewer_ids = set()
446 # members / reviewers
446 # members / reviewers
447 for reviewer_object in reviewers:
447 for reviewer_object in reviewers:
448 user_id, reasons, mandatory = reviewer_object
448 user_id, reasons, mandatory = reviewer_object
449 user = self._get_user(user_id)
449 user = self._get_user(user_id)
450
450
451 # skip duplicates
451 # skip duplicates
452 if user.user_id in reviewer_ids:
452 if user.user_id in reviewer_ids:
453 continue
453 continue
454
454
455 reviewer_ids.add(user.user_id)
455 reviewer_ids.add(user.user_id)
456
456
457 reviewer = PullRequestReviewers()
457 reviewer = PullRequestReviewers()
458 reviewer.user = user
458 reviewer.user = user
459 reviewer.pull_request = pull_request
459 reviewer.pull_request = pull_request
460 reviewer.reasons = reasons
460 reviewer.reasons = reasons
461 reviewer.mandatory = mandatory
461 reviewer.mandatory = mandatory
462 Session().add(reviewer)
462 Session().add(reviewer)
463
463
464 # Set approval status to "Under Review" for all commits which are
464 # Set approval status to "Under Review" for all commits which are
465 # part of this pull request.
465 # part of this pull request.
466 ChangesetStatusModel().set_status(
466 ChangesetStatusModel().set_status(
467 repo=target_repo,
467 repo=target_repo,
468 status=ChangesetStatus.STATUS_UNDER_REVIEW,
468 status=ChangesetStatus.STATUS_UNDER_REVIEW,
469 user=created_by_user,
469 user=created_by_user,
470 pull_request=pull_request
470 pull_request=pull_request
471 )
471 )
472
472
473 MergeCheck.validate(
473 MergeCheck.validate(
474 pull_request, user=created_by_user, translator=translator)
474 pull_request, user=created_by_user, translator=translator)
475
475
476 self.notify_reviewers(pull_request, reviewer_ids)
476 self.notify_reviewers(pull_request, reviewer_ids)
477 self._trigger_pull_request_hook(
477 self._trigger_pull_request_hook(
478 pull_request, created_by_user, 'create')
478 pull_request, created_by_user, 'create')
479
479
480 creation_data = pull_request.get_api_data(with_merge_state=False)
480 creation_data = pull_request.get_api_data(with_merge_state=False)
481 self._log_audit_action(
481 self._log_audit_action(
482 'repo.pull_request.create', {'data': creation_data},
482 'repo.pull_request.create', {'data': creation_data},
483 created_by_user, pull_request)
483 created_by_user, pull_request)
484
484
485 return pull_request
485 return pull_request
486
486
487 def _trigger_pull_request_hook(self, pull_request, user, action):
487 def _trigger_pull_request_hook(self, pull_request, user, action):
488 pull_request = self.__get_pull_request(pull_request)
488 pull_request = self.__get_pull_request(pull_request)
489 target_scm = pull_request.target_repo.scm_instance()
489 target_scm = pull_request.target_repo.scm_instance()
490 if action == 'create':
490 if action == 'create':
491 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
491 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
492 elif action == 'merge':
492 elif action == 'merge':
493 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
493 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
494 elif action == 'close':
494 elif action == 'close':
495 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
495 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
496 elif action == 'review_status_change':
496 elif action == 'review_status_change':
497 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
497 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
498 elif action == 'update':
498 elif action == 'update':
499 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
499 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
500 else:
500 else:
501 return
501 return
502
502
503 trigger_hook(
503 trigger_hook(
504 username=user.username,
504 username=user.username,
505 repo_name=pull_request.target_repo.repo_name,
505 repo_name=pull_request.target_repo.repo_name,
506 repo_alias=target_scm.alias,
506 repo_alias=target_scm.alias,
507 pull_request=pull_request)
507 pull_request=pull_request)
508
508
509 def _get_commit_ids(self, pull_request):
509 def _get_commit_ids(self, pull_request):
510 """
510 """
511 Return the commit ids of the merged pull request.
511 Return the commit ids of the merged pull request.
512
512
513 This method is not dealing correctly yet with the lack of autoupdates
513 This method is not dealing correctly yet with the lack of autoupdates
514 nor with the implicit target updates.
514 nor with the implicit target updates.
515 For example: if a commit in the source repo is already in the target it
515 For example: if a commit in the source repo is already in the target it
516 will be reported anyways.
516 will be reported anyways.
517 """
517 """
518 merge_rev = pull_request.merge_rev
518 merge_rev = pull_request.merge_rev
519 if merge_rev is None:
519 if merge_rev is None:
520 raise ValueError('This pull request was not merged yet')
520 raise ValueError('This pull request was not merged yet')
521
521
522 commit_ids = list(pull_request.revisions)
522 commit_ids = list(pull_request.revisions)
523 if merge_rev not in commit_ids:
523 if merge_rev not in commit_ids:
524 commit_ids.append(merge_rev)
524 commit_ids.append(merge_rev)
525
525
526 return commit_ids
526 return commit_ids
527
527
528 def merge(self, pull_request, user, extras):
528 def merge(self, pull_request, user, extras):
529 log.debug("Merging pull request %s", pull_request.pull_request_id)
529 log.debug("Merging pull request %s", pull_request.pull_request_id)
530 merge_state = self._merge_pull_request(pull_request, user, extras)
530 merge_state = self._merge_pull_request(pull_request, user, extras)
531 if merge_state.executed:
531 if merge_state.executed:
532 log.debug(
532 log.debug(
533 "Merge was successful, updating the pull request comments.")
533 "Merge was successful, updating the pull request comments.")
534 self._comment_and_close_pr(pull_request, user, merge_state)
534 self._comment_and_close_pr(pull_request, user, merge_state)
535
535
536 self._log_audit_action(
536 self._log_audit_action(
537 'repo.pull_request.merge',
537 'repo.pull_request.merge',
538 {'merge_state': merge_state.__dict__},
538 {'merge_state': merge_state.__dict__},
539 user, pull_request)
539 user, pull_request)
540
540
541 else:
541 else:
542 log.warn("Merge failed, not updating the pull request.")
542 log.warn("Merge failed, not updating the pull request.")
543 return merge_state
543 return merge_state
544
544
545 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
545 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
546 target_vcs = pull_request.target_repo.scm_instance()
546 target_vcs = pull_request.target_repo.scm_instance()
547 source_vcs = pull_request.source_repo.scm_instance()
547 source_vcs = pull_request.source_repo.scm_instance()
548 target_ref = self._refresh_reference(
548 target_ref = self._refresh_reference(
549 pull_request.target_ref_parts, target_vcs)
549 pull_request.target_ref_parts, target_vcs)
550
550
551 message = merge_msg or (
551 message = merge_msg or (
552 'Merge pull request #%(pr_id)s from '
552 'Merge pull request #%(pr_id)s from '
553 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
553 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
554 'pr_id': pull_request.pull_request_id,
554 'pr_id': pull_request.pull_request_id,
555 'source_repo': source_vcs.name,
555 'source_repo': source_vcs.name,
556 'source_ref_name': pull_request.source_ref_parts.name,
556 'source_ref_name': pull_request.source_ref_parts.name,
557 'pr_title': pull_request.title
557 'pr_title': pull_request.title
558 }
558 }
559
559
560 workspace_id = self._workspace_id(pull_request)
560 workspace_id = self._workspace_id(pull_request)
561 use_rebase = self._use_rebase_for_merging(pull_request)
561 use_rebase = self._use_rebase_for_merging(pull_request)
562 close_branch = self._close_branch_before_merging(pull_request)
562 close_branch = self._close_branch_before_merging(pull_request)
563
563
564 callback_daemon, extras = prepare_callback_daemon(
564 callback_daemon, extras = prepare_callback_daemon(
565 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
565 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
566 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
566 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
567
567
568 with callback_daemon:
568 with callback_daemon:
569 # TODO: johbo: Implement a clean way to run a config_override
569 # TODO: johbo: Implement a clean way to run a config_override
570 # for a single call.
570 # for a single call.
571 target_vcs.config.set(
571 target_vcs.config.set(
572 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
572 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
573 merge_state = target_vcs.merge(
573 merge_state = target_vcs.merge(
574 target_ref, source_vcs, pull_request.source_ref_parts,
574 target_ref, source_vcs, pull_request.source_ref_parts,
575 workspace_id, user_name=user.username,
575 workspace_id, user_name=user.username,
576 user_email=user.email, message=message, use_rebase=use_rebase,
576 user_email=user.email, message=message, use_rebase=use_rebase,
577 close_branch=close_branch)
577 close_branch=close_branch)
578 return merge_state
578 return merge_state
579
579
580 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
580 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
581 pull_request.merge_rev = merge_state.merge_ref.commit_id
581 pull_request.merge_rev = merge_state.merge_ref.commit_id
582 pull_request.updated_on = datetime.datetime.now()
582 pull_request.updated_on = datetime.datetime.now()
583 close_msg = close_msg or 'Pull request merged and closed'
583 close_msg = close_msg or 'Pull request merged and closed'
584
584
585 CommentsModel().create(
585 CommentsModel().create(
586 text=safe_unicode(close_msg),
586 text=safe_unicode(close_msg),
587 repo=pull_request.target_repo.repo_id,
587 repo=pull_request.target_repo.repo_id,
588 user=user.user_id,
588 user=user.user_id,
589 pull_request=pull_request.pull_request_id,
589 pull_request=pull_request.pull_request_id,
590 f_path=None,
590 f_path=None,
591 line_no=None,
591 line_no=None,
592 closing_pr=True
592 closing_pr=True
593 )
593 )
594
594
595 Session().add(pull_request)
595 Session().add(pull_request)
596 Session().flush()
596 Session().flush()
597 # TODO: paris: replace invalidation with less radical solution
597 # TODO: paris: replace invalidation with less radical solution
598 ScmModel().mark_for_invalidation(
598 ScmModel().mark_for_invalidation(
599 pull_request.target_repo.repo_name)
599 pull_request.target_repo.repo_name)
600 self._trigger_pull_request_hook(pull_request, user, 'merge')
600 self._trigger_pull_request_hook(pull_request, user, 'merge')
601
601
602 def has_valid_update_type(self, pull_request):
602 def has_valid_update_type(self, pull_request):
603 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
604 return source_ref_type in ['book', 'branch', 'tag']
604 return source_ref_type in ['book', 'branch', 'tag']
605
605
606 def update_commits(self, pull_request):
606 def update_commits(self, pull_request):
607 """
607 """
608 Get the updated list of commits for the pull request
608 Get the updated list of commits for the pull request
609 and return the new pull request version and the list
609 and return the new pull request version and the list
610 of commits processed by this update action
610 of commits processed by this update action
611 """
611 """
612 pull_request = self.__get_pull_request(pull_request)
612 pull_request = self.__get_pull_request(pull_request)
613 source_ref_type = pull_request.source_ref_parts.type
613 source_ref_type = pull_request.source_ref_parts.type
614 source_ref_name = pull_request.source_ref_parts.name
614 source_ref_name = pull_request.source_ref_parts.name
615 source_ref_id = pull_request.source_ref_parts.commit_id
615 source_ref_id = pull_request.source_ref_parts.commit_id
616
616
617 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_type = pull_request.target_ref_parts.type
618 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_name = pull_request.target_ref_parts.name
619 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_ref_id = pull_request.target_ref_parts.commit_id
620
620
621 if not self.has_valid_update_type(pull_request):
621 if not self.has_valid_update_type(pull_request):
622 log.debug(
622 log.debug(
623 "Skipping update of pull request %s due to ref type: %s",
623 "Skipping update of pull request %s due to ref type: %s",
624 pull_request, source_ref_type)
624 pull_request, source_ref_type)
625 return UpdateResponse(
625 return UpdateResponse(
626 executed=False,
626 executed=False,
627 reason=UpdateFailureReason.WRONG_REF_TYPE,
627 reason=UpdateFailureReason.WRONG_REF_TYPE,
628 old=pull_request, new=None, changes=None,
628 old=pull_request, new=None, changes=None,
629 source_changed=False, target_changed=False)
629 source_changed=False, target_changed=False)
630
630
631 # source repo
631 # source repo
632 source_repo = pull_request.source_repo.scm_instance()
632 source_repo = pull_request.source_repo.scm_instance()
633 try:
633 try:
634 source_commit = source_repo.get_commit(commit_id=source_ref_name)
634 source_commit = source_repo.get_commit(commit_id=source_ref_name)
635 except CommitDoesNotExistError:
635 except CommitDoesNotExistError:
636 return UpdateResponse(
636 return UpdateResponse(
637 executed=False,
637 executed=False,
638 reason=UpdateFailureReason.MISSING_SOURCE_REF,
638 reason=UpdateFailureReason.MISSING_SOURCE_REF,
639 old=pull_request, new=None, changes=None,
639 old=pull_request, new=None, changes=None,
640 source_changed=False, target_changed=False)
640 source_changed=False, target_changed=False)
641
641
642 source_changed = source_ref_id != source_commit.raw_id
642 source_changed = source_ref_id != source_commit.raw_id
643
643
644 # target repo
644 # target repo
645 target_repo = pull_request.target_repo.scm_instance()
645 target_repo = pull_request.target_repo.scm_instance()
646 try:
646 try:
647 target_commit = target_repo.get_commit(commit_id=target_ref_name)
647 target_commit = target_repo.get_commit(commit_id=target_ref_name)
648 except CommitDoesNotExistError:
648 except CommitDoesNotExistError:
649 return UpdateResponse(
649 return UpdateResponse(
650 executed=False,
650 executed=False,
651 reason=UpdateFailureReason.MISSING_TARGET_REF,
651 reason=UpdateFailureReason.MISSING_TARGET_REF,
652 old=pull_request, new=None, changes=None,
652 old=pull_request, new=None, changes=None,
653 source_changed=False, target_changed=False)
653 source_changed=False, target_changed=False)
654 target_changed = target_ref_id != target_commit.raw_id
654 target_changed = target_ref_id != target_commit.raw_id
655
655
656 if not (source_changed or target_changed):
656 if not (source_changed or target_changed):
657 log.debug("Nothing changed in pull request %s", pull_request)
657 log.debug("Nothing changed in pull request %s", pull_request)
658 return UpdateResponse(
658 return UpdateResponse(
659 executed=False,
659 executed=False,
660 reason=UpdateFailureReason.NO_CHANGE,
660 reason=UpdateFailureReason.NO_CHANGE,
661 old=pull_request, new=None, changes=None,
661 old=pull_request, new=None, changes=None,
662 source_changed=target_changed, target_changed=source_changed)
662 source_changed=target_changed, target_changed=source_changed)
663
663
664 change_in_found = 'target repo' if target_changed else 'source repo'
664 change_in_found = 'target repo' if target_changed else 'source repo'
665 log.debug('Updating pull request because of change in %s detected',
665 log.debug('Updating pull request because of change in %s detected',
666 change_in_found)
666 change_in_found)
667
667
668 # Finally there is a need for an update, in case of source change
668 # Finally there is a need for an update, in case of source change
669 # we create a new version, else just an update
669 # we create a new version, else just an update
670 if source_changed:
670 if source_changed:
671 pull_request_version = self._create_version_from_snapshot(pull_request)
671 pull_request_version = self._create_version_from_snapshot(pull_request)
672 self._link_comments_to_version(pull_request_version)
672 self._link_comments_to_version(pull_request_version)
673 else:
673 else:
674 try:
674 try:
675 ver = pull_request.versions[-1]
675 ver = pull_request.versions[-1]
676 except IndexError:
676 except IndexError:
677 ver = None
677 ver = None
678
678
679 pull_request.pull_request_version_id = \
679 pull_request.pull_request_version_id = \
680 ver.pull_request_version_id if ver else None
680 ver.pull_request_version_id if ver else None
681 pull_request_version = pull_request
681 pull_request_version = pull_request
682
682
683 try:
683 try:
684 if target_ref_type in ('tag', 'branch', 'book'):
684 if target_ref_type in ('tag', 'branch', 'book'):
685 target_commit = target_repo.get_commit(target_ref_name)
685 target_commit = target_repo.get_commit(target_ref_name)
686 else:
686 else:
687 target_commit = target_repo.get_commit(target_ref_id)
687 target_commit = target_repo.get_commit(target_ref_id)
688 except CommitDoesNotExistError:
688 except CommitDoesNotExistError:
689 return UpdateResponse(
689 return UpdateResponse(
690 executed=False,
690 executed=False,
691 reason=UpdateFailureReason.MISSING_TARGET_REF,
691 reason=UpdateFailureReason.MISSING_TARGET_REF,
692 old=pull_request, new=None, changes=None,
692 old=pull_request, new=None, changes=None,
693 source_changed=source_changed, target_changed=target_changed)
693 source_changed=source_changed, target_changed=target_changed)
694
694
695 # re-compute commit ids
695 # re-compute commit ids
696 old_commit_ids = pull_request.revisions
696 old_commit_ids = pull_request.revisions
697 pre_load = ["author", "branch", "date", "message"]
697 pre_load = ["author", "branch", "date", "message"]
698 commit_ranges = target_repo.compare(
698 commit_ranges = target_repo.compare(
699 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
699 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
700 pre_load=pre_load)
700 pre_load=pre_load)
701
701
702 ancestor = target_repo.get_common_ancestor(
702 ancestor = target_repo.get_common_ancestor(
703 target_commit.raw_id, source_commit.raw_id, source_repo)
703 target_commit.raw_id, source_commit.raw_id, source_repo)
704
704
705 pull_request.source_ref = '%s:%s:%s' % (
705 pull_request.source_ref = '%s:%s:%s' % (
706 source_ref_type, source_ref_name, source_commit.raw_id)
706 source_ref_type, source_ref_name, source_commit.raw_id)
707 pull_request.target_ref = '%s:%s:%s' % (
707 pull_request.target_ref = '%s:%s:%s' % (
708 target_ref_type, target_ref_name, ancestor)
708 target_ref_type, target_ref_name, ancestor)
709
709
710 pull_request.revisions = [
710 pull_request.revisions = [
711 commit.raw_id for commit in reversed(commit_ranges)]
711 commit.raw_id for commit in reversed(commit_ranges)]
712 pull_request.updated_on = datetime.datetime.now()
712 pull_request.updated_on = datetime.datetime.now()
713 Session().add(pull_request)
713 Session().add(pull_request)
714 new_commit_ids = pull_request.revisions
714 new_commit_ids = pull_request.revisions
715
715
716 old_diff_data, new_diff_data = self._generate_update_diffs(
716 old_diff_data, new_diff_data = self._generate_update_diffs(
717 pull_request, pull_request_version)
717 pull_request, pull_request_version)
718
718
719 # calculate commit and file changes
719 # calculate commit and file changes
720 changes = self._calculate_commit_id_changes(
720 changes = self._calculate_commit_id_changes(
721 old_commit_ids, new_commit_ids)
721 old_commit_ids, new_commit_ids)
722 file_changes = self._calculate_file_changes(
722 file_changes = self._calculate_file_changes(
723 old_diff_data, new_diff_data)
723 old_diff_data, new_diff_data)
724
724
725 # set comments as outdated if DIFFS changed
725 # set comments as outdated if DIFFS changed
726 CommentsModel().outdate_comments(
726 CommentsModel().outdate_comments(
727 pull_request, old_diff_data=old_diff_data,
727 pull_request, old_diff_data=old_diff_data,
728 new_diff_data=new_diff_data)
728 new_diff_data=new_diff_data)
729
729
730 commit_changes = (changes.added or changes.removed)
730 commit_changes = (changes.added or changes.removed)
731 file_node_changes = (
731 file_node_changes = (
732 file_changes.added or file_changes.modified or file_changes.removed)
732 file_changes.added or file_changes.modified or file_changes.removed)
733 pr_has_changes = commit_changes or file_node_changes
733 pr_has_changes = commit_changes or file_node_changes
734
734
735 # Add an automatic comment to the pull request, in case
735 # Add an automatic comment to the pull request, in case
736 # anything has changed
736 # anything has changed
737 if pr_has_changes:
737 if pr_has_changes:
738 update_comment = CommentsModel().create(
738 update_comment = CommentsModel().create(
739 text=self._render_update_message(changes, file_changes),
739 text=self._render_update_message(changes, file_changes),
740 repo=pull_request.target_repo,
740 repo=pull_request.target_repo,
741 user=pull_request.author,
741 user=pull_request.author,
742 pull_request=pull_request,
742 pull_request=pull_request,
743 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
743 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
744
744
745 # Update status to "Under Review" for added commits
745 # Update status to "Under Review" for added commits
746 for commit_id in changes.added:
746 for commit_id in changes.added:
747 ChangesetStatusModel().set_status(
747 ChangesetStatusModel().set_status(
748 repo=pull_request.source_repo,
748 repo=pull_request.source_repo,
749 status=ChangesetStatus.STATUS_UNDER_REVIEW,
749 status=ChangesetStatus.STATUS_UNDER_REVIEW,
750 comment=update_comment,
750 comment=update_comment,
751 user=pull_request.author,
751 user=pull_request.author,
752 pull_request=pull_request,
752 pull_request=pull_request,
753 revision=commit_id)
753 revision=commit_id)
754
754
755 log.debug(
755 log.debug(
756 'Updated pull request %s, added_ids: %s, common_ids: %s, '
756 'Updated pull request %s, added_ids: %s, common_ids: %s, '
757 'removed_ids: %s', pull_request.pull_request_id,
757 'removed_ids: %s', pull_request.pull_request_id,
758 changes.added, changes.common, changes.removed)
758 changes.added, changes.common, changes.removed)
759 log.debug(
759 log.debug(
760 'Updated pull request with the following file changes: %s',
760 'Updated pull request with the following file changes: %s',
761 file_changes)
761 file_changes)
762
762
763 log.info(
763 log.info(
764 "Updated pull request %s from commit %s to commit %s, "
764 "Updated pull request %s from commit %s to commit %s, "
765 "stored new version %s of this pull request.",
765 "stored new version %s of this pull request.",
766 pull_request.pull_request_id, source_ref_id,
766 pull_request.pull_request_id, source_ref_id,
767 pull_request.source_ref_parts.commit_id,
767 pull_request.source_ref_parts.commit_id,
768 pull_request_version.pull_request_version_id)
768 pull_request_version.pull_request_version_id)
769 Session().commit()
769 Session().commit()
770 self._trigger_pull_request_hook(
770 self._trigger_pull_request_hook(
771 pull_request, pull_request.author, 'update')
771 pull_request, pull_request.author, 'update')
772
772
773 return UpdateResponse(
773 return UpdateResponse(
774 executed=True, reason=UpdateFailureReason.NONE,
774 executed=True, reason=UpdateFailureReason.NONE,
775 old=pull_request, new=pull_request_version, changes=changes,
775 old=pull_request, new=pull_request_version, changes=changes,
776 source_changed=source_changed, target_changed=target_changed)
776 source_changed=source_changed, target_changed=target_changed)
777
777
778 def _create_version_from_snapshot(self, pull_request):
778 def _create_version_from_snapshot(self, pull_request):
779 version = PullRequestVersion()
779 version = PullRequestVersion()
780 version.title = pull_request.title
780 version.title = pull_request.title
781 version.description = pull_request.description
781 version.description = pull_request.description
782 version.status = pull_request.status
782 version.status = pull_request.status
783 version.created_on = datetime.datetime.now()
783 version.created_on = datetime.datetime.now()
784 version.updated_on = pull_request.updated_on
784 version.updated_on = pull_request.updated_on
785 version.user_id = pull_request.user_id
785 version.user_id = pull_request.user_id
786 version.source_repo = pull_request.source_repo
786 version.source_repo = pull_request.source_repo
787 version.source_ref = pull_request.source_ref
787 version.source_ref = pull_request.source_ref
788 version.target_repo = pull_request.target_repo
788 version.target_repo = pull_request.target_repo
789 version.target_ref = pull_request.target_ref
789 version.target_ref = pull_request.target_ref
790
790
791 version._last_merge_source_rev = pull_request._last_merge_source_rev
791 version._last_merge_source_rev = pull_request._last_merge_source_rev
792 version._last_merge_target_rev = pull_request._last_merge_target_rev
792 version._last_merge_target_rev = pull_request._last_merge_target_rev
793 version.last_merge_status = pull_request.last_merge_status
793 version.last_merge_status = pull_request.last_merge_status
794 version.shadow_merge_ref = pull_request.shadow_merge_ref
794 version.shadow_merge_ref = pull_request.shadow_merge_ref
795 version.merge_rev = pull_request.merge_rev
795 version.merge_rev = pull_request.merge_rev
796 version.reviewer_data = pull_request.reviewer_data
796 version.reviewer_data = pull_request.reviewer_data
797
797
798 version.revisions = pull_request.revisions
798 version.revisions = pull_request.revisions
799 version.pull_request = pull_request
799 version.pull_request = pull_request
800 Session().add(version)
800 Session().add(version)
801 Session().flush()
801 Session().flush()
802
802
803 return version
803 return version
804
804
805 def _generate_update_diffs(self, pull_request, pull_request_version):
805 def _generate_update_diffs(self, pull_request, pull_request_version):
806
806
807 diff_context = (
807 diff_context = (
808 self.DIFF_CONTEXT +
808 self.DIFF_CONTEXT +
809 CommentsModel.needed_extra_diff_context())
809 CommentsModel.needed_extra_diff_context())
810
810
811 source_repo = pull_request_version.source_repo
811 source_repo = pull_request_version.source_repo
812 source_ref_id = pull_request_version.source_ref_parts.commit_id
812 source_ref_id = pull_request_version.source_ref_parts.commit_id
813 target_ref_id = pull_request_version.target_ref_parts.commit_id
813 target_ref_id = pull_request_version.target_ref_parts.commit_id
814 old_diff = self._get_diff_from_pr_or_version(
814 old_diff = self._get_diff_from_pr_or_version(
815 source_repo, source_ref_id, target_ref_id, context=diff_context)
815 source_repo, source_ref_id, target_ref_id, context=diff_context)
816
816
817 source_repo = pull_request.source_repo
817 source_repo = pull_request.source_repo
818 source_ref_id = pull_request.source_ref_parts.commit_id
818 source_ref_id = pull_request.source_ref_parts.commit_id
819 target_ref_id = pull_request.target_ref_parts.commit_id
819 target_ref_id = pull_request.target_ref_parts.commit_id
820
820
821 new_diff = self._get_diff_from_pr_or_version(
821 new_diff = self._get_diff_from_pr_or_version(
822 source_repo, source_ref_id, target_ref_id, context=diff_context)
822 source_repo, source_ref_id, target_ref_id, context=diff_context)
823
823
824 old_diff_data = diffs.DiffProcessor(old_diff)
824 old_diff_data = diffs.DiffProcessor(old_diff)
825 old_diff_data.prepare()
825 old_diff_data.prepare()
826 new_diff_data = diffs.DiffProcessor(new_diff)
826 new_diff_data = diffs.DiffProcessor(new_diff)
827 new_diff_data.prepare()
827 new_diff_data.prepare()
828
828
829 return old_diff_data, new_diff_data
829 return old_diff_data, new_diff_data
830
830
831 def _link_comments_to_version(self, pull_request_version):
831 def _link_comments_to_version(self, pull_request_version):
832 """
832 """
833 Link all unlinked comments of this pull request to the given version.
833 Link all unlinked comments of this pull request to the given version.
834
834
835 :param pull_request_version: The `PullRequestVersion` to which
835 :param pull_request_version: The `PullRequestVersion` to which
836 the comments shall be linked.
836 the comments shall be linked.
837
837
838 """
838 """
839 pull_request = pull_request_version.pull_request
839 pull_request = pull_request_version.pull_request
840 comments = ChangesetComment.query()\
840 comments = ChangesetComment.query()\
841 .filter(
841 .filter(
842 # TODO: johbo: Should we query for the repo at all here?
842 # TODO: johbo: Should we query for the repo at all here?
843 # Pending decision on how comments of PRs are to be related
843 # Pending decision on how comments of PRs are to be related
844 # to either the source repo, the target repo or no repo at all.
844 # to either the source repo, the target repo or no repo at all.
845 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
845 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
846 ChangesetComment.pull_request == pull_request,
846 ChangesetComment.pull_request == pull_request,
847 ChangesetComment.pull_request_version == None)\
847 ChangesetComment.pull_request_version == None)\
848 .order_by(ChangesetComment.comment_id.asc())
848 .order_by(ChangesetComment.comment_id.asc())
849
849
850 # TODO: johbo: Find out why this breaks if it is done in a bulk
850 # TODO: johbo: Find out why this breaks if it is done in a bulk
851 # operation.
851 # operation.
852 for comment in comments:
852 for comment in comments:
853 comment.pull_request_version_id = (
853 comment.pull_request_version_id = (
854 pull_request_version.pull_request_version_id)
854 pull_request_version.pull_request_version_id)
855 Session().add(comment)
855 Session().add(comment)
856
856
857 def _calculate_commit_id_changes(self, old_ids, new_ids):
857 def _calculate_commit_id_changes(self, old_ids, new_ids):
858 added = [x for x in new_ids if x not in old_ids]
858 added = [x for x in new_ids if x not in old_ids]
859 common = [x for x in new_ids if x in old_ids]
859 common = [x for x in new_ids if x in old_ids]
860 removed = [x for x in old_ids if x not in new_ids]
860 removed = [x for x in old_ids if x not in new_ids]
861 total = new_ids
861 total = new_ids
862 return ChangeTuple(added, common, removed, total)
862 return ChangeTuple(added, common, removed, total)
863
863
864 def _calculate_file_changes(self, old_diff_data, new_diff_data):
864 def _calculate_file_changes(self, old_diff_data, new_diff_data):
865
865
866 old_files = OrderedDict()
866 old_files = OrderedDict()
867 for diff_data in old_diff_data.parsed_diff:
867 for diff_data in old_diff_data.parsed_diff:
868 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
868 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
869
869
870 added_files = []
870 added_files = []
871 modified_files = []
871 modified_files = []
872 removed_files = []
872 removed_files = []
873 for diff_data in new_diff_data.parsed_diff:
873 for diff_data in new_diff_data.parsed_diff:
874 new_filename = diff_data['filename']
874 new_filename = diff_data['filename']
875 new_hash = md5_safe(diff_data['raw_diff'])
875 new_hash = md5_safe(diff_data['raw_diff'])
876
876
877 old_hash = old_files.get(new_filename)
877 old_hash = old_files.get(new_filename)
878 if not old_hash:
878 if not old_hash:
879 # file is not present in old diff, means it's added
879 # file is not present in old diff, means it's added
880 added_files.append(new_filename)
880 added_files.append(new_filename)
881 else:
881 else:
882 if new_hash != old_hash:
882 if new_hash != old_hash:
883 modified_files.append(new_filename)
883 modified_files.append(new_filename)
884 # now remove a file from old, since we have seen it already
884 # now remove a file from old, since we have seen it already
885 del old_files[new_filename]
885 del old_files[new_filename]
886
886
887 # removed files is when there are present in old, but not in NEW,
887 # removed files is when there are present in old, but not in NEW,
888 # since we remove old files that are present in new diff, left-overs
888 # since we remove old files that are present in new diff, left-overs
889 # if any should be the removed files
889 # if any should be the removed files
890 removed_files.extend(old_files.keys())
890 removed_files.extend(old_files.keys())
891
891
892 return FileChangeTuple(added_files, modified_files, removed_files)
892 return FileChangeTuple(added_files, modified_files, removed_files)
893
893
894 def _render_update_message(self, changes, file_changes):
894 def _render_update_message(self, changes, file_changes):
895 """
895 """
896 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
896 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
897 so it's always looking the same disregarding on which default
897 so it's always looking the same disregarding on which default
898 renderer system is using.
898 renderer system is using.
899
899
900 :param changes: changes named tuple
900 :param changes: changes named tuple
901 :param file_changes: file changes named tuple
901 :param file_changes: file changes named tuple
902
902
903 """
903 """
904 new_status = ChangesetStatus.get_status_lbl(
904 new_status = ChangesetStatus.get_status_lbl(
905 ChangesetStatus.STATUS_UNDER_REVIEW)
905 ChangesetStatus.STATUS_UNDER_REVIEW)
906
906
907 changed_files = (
907 changed_files = (
908 file_changes.added + file_changes.modified + file_changes.removed)
908 file_changes.added + file_changes.modified + file_changes.removed)
909
909
910 params = {
910 params = {
911 'under_review_label': new_status,
911 'under_review_label': new_status,
912 'added_commits': changes.added,
912 'added_commits': changes.added,
913 'removed_commits': changes.removed,
913 'removed_commits': changes.removed,
914 'changed_files': changed_files,
914 'changed_files': changed_files,
915 'added_files': file_changes.added,
915 'added_files': file_changes.added,
916 'modified_files': file_changes.modified,
916 'modified_files': file_changes.modified,
917 'removed_files': file_changes.removed,
917 'removed_files': file_changes.removed,
918 }
918 }
919 renderer = RstTemplateRenderer()
919 renderer = RstTemplateRenderer()
920 return renderer.render('pull_request_update.mako', **params)
920 return renderer.render('pull_request_update.mako', **params)
921
921
922 def edit(self, pull_request, title, description, user):
922 def edit(self, pull_request, title, description, user):
923 pull_request = self.__get_pull_request(pull_request)
923 pull_request = self.__get_pull_request(pull_request)
924 old_data = pull_request.get_api_data(with_merge_state=False)
924 old_data = pull_request.get_api_data(with_merge_state=False)
925 if pull_request.is_closed():
925 if pull_request.is_closed():
926 raise ValueError('This pull request is closed')
926 raise ValueError('This pull request is closed')
927 if title:
927 if title:
928 pull_request.title = title
928 pull_request.title = title
929 pull_request.description = description
929 pull_request.description = description
930 pull_request.updated_on = datetime.datetime.now()
930 pull_request.updated_on = datetime.datetime.now()
931 Session().add(pull_request)
931 Session().add(pull_request)
932 self._log_audit_action(
932 self._log_audit_action(
933 'repo.pull_request.edit', {'old_data': old_data},
933 'repo.pull_request.edit', {'old_data': old_data},
934 user, pull_request)
934 user, pull_request)
935
935
936 def update_reviewers(self, pull_request, reviewer_data, user):
936 def update_reviewers(self, pull_request, reviewer_data, user):
937 """
937 """
938 Update the reviewers in the pull request
938 Update the reviewers in the pull request
939
939
940 :param pull_request: the pr to update
940 :param pull_request: the pr to update
941 :param reviewer_data: list of tuples
941 :param reviewer_data: list of tuples
942 [(user, ['reason1', 'reason2'], mandatory_flag)]
942 [(user, ['reason1', 'reason2'], mandatory_flag)]
943 """
943 """
944 pull_request = self.__get_pull_request(pull_request)
945 if pull_request.is_closed():
946 raise ValueError('This pull request is closed')
944
947
945 reviewers = {}
948 reviewers = {}
946 for user_id, reasons, mandatory in reviewer_data:
949 for user_id, reasons, mandatory in reviewer_data:
947 if isinstance(user_id, (int, basestring)):
950 if isinstance(user_id, (int, basestring)):
948 user_id = self._get_user(user_id).user_id
951 user_id = self._get_user(user_id).user_id
949 reviewers[user_id] = {
952 reviewers[user_id] = {
950 'reasons': reasons, 'mandatory': mandatory}
953 'reasons': reasons, 'mandatory': mandatory}
951
954
952 reviewers_ids = set(reviewers.keys())
955 reviewers_ids = set(reviewers.keys())
953 pull_request = self.__get_pull_request(pull_request)
954 current_reviewers = PullRequestReviewers.query()\
956 current_reviewers = PullRequestReviewers.query()\
955 .filter(PullRequestReviewers.pull_request ==
957 .filter(PullRequestReviewers.pull_request ==
956 pull_request).all()
958 pull_request).all()
957 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
959 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
958
960
959 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
961 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
960 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
962 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
961
963
962 log.debug("Adding %s reviewers", ids_to_add)
964 log.debug("Adding %s reviewers", ids_to_add)
963 log.debug("Removing %s reviewers", ids_to_remove)
965 log.debug("Removing %s reviewers", ids_to_remove)
964 changed = False
966 changed = False
965 for uid in ids_to_add:
967 for uid in ids_to_add:
966 changed = True
968 changed = True
967 _usr = self._get_user(uid)
969 _usr = self._get_user(uid)
968 reviewer = PullRequestReviewers()
970 reviewer = PullRequestReviewers()
969 reviewer.user = _usr
971 reviewer.user = _usr
970 reviewer.pull_request = pull_request
972 reviewer.pull_request = pull_request
971 reviewer.reasons = reviewers[uid]['reasons']
973 reviewer.reasons = reviewers[uid]['reasons']
972 # NOTE(marcink): mandatory shouldn't be changed now
974 # NOTE(marcink): mandatory shouldn't be changed now
973 # reviewer.mandatory = reviewers[uid]['reasons']
975 # reviewer.mandatory = reviewers[uid]['reasons']
974 Session().add(reviewer)
976 Session().add(reviewer)
975 self._log_audit_action(
977 self._log_audit_action(
976 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
978 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
977 user, pull_request)
979 user, pull_request)
978
980
979 for uid in ids_to_remove:
981 for uid in ids_to_remove:
980 changed = True
982 changed = True
981 reviewers = PullRequestReviewers.query()\
983 reviewers = PullRequestReviewers.query()\
982 .filter(PullRequestReviewers.user_id == uid,
984 .filter(PullRequestReviewers.user_id == uid,
983 PullRequestReviewers.pull_request == pull_request)\
985 PullRequestReviewers.pull_request == pull_request)\
984 .all()
986 .all()
985 # use .all() in case we accidentally added the same person twice
987 # use .all() in case we accidentally added the same person twice
986 # this CAN happen due to the lack of DB checks
988 # this CAN happen due to the lack of DB checks
987 for obj in reviewers:
989 for obj in reviewers:
988 old_data = obj.get_dict()
990 old_data = obj.get_dict()
989 Session().delete(obj)
991 Session().delete(obj)
990 self._log_audit_action(
992 self._log_audit_action(
991 'repo.pull_request.reviewer.delete',
993 'repo.pull_request.reviewer.delete',
992 {'old_data': old_data}, user, pull_request)
994 {'old_data': old_data}, user, pull_request)
993
995
994 if changed:
996 if changed:
995 pull_request.updated_on = datetime.datetime.now()
997 pull_request.updated_on = datetime.datetime.now()
996 Session().add(pull_request)
998 Session().add(pull_request)
997
999
998 self.notify_reviewers(pull_request, ids_to_add)
1000 self.notify_reviewers(pull_request, ids_to_add)
999 return ids_to_add, ids_to_remove
1001 return ids_to_add, ids_to_remove
1000
1002
1001 def get_url(self, pull_request, request=None, permalink=False):
1003 def get_url(self, pull_request, request=None, permalink=False):
1002 if not request:
1004 if not request:
1003 request = get_current_request()
1005 request = get_current_request()
1004
1006
1005 if permalink:
1007 if permalink:
1006 return request.route_url(
1008 return request.route_url(
1007 'pull_requests_global',
1009 'pull_requests_global',
1008 pull_request_id=pull_request.pull_request_id,)
1010 pull_request_id=pull_request.pull_request_id,)
1009 else:
1011 else:
1010 return request.route_url('pullrequest_show',
1012 return request.route_url('pullrequest_show',
1011 repo_name=safe_str(pull_request.target_repo.repo_name),
1013 repo_name=safe_str(pull_request.target_repo.repo_name),
1012 pull_request_id=pull_request.pull_request_id,)
1014 pull_request_id=pull_request.pull_request_id,)
1013
1015
1014 def get_shadow_clone_url(self, pull_request):
1016 def get_shadow_clone_url(self, pull_request):
1015 """
1017 """
1016 Returns qualified url pointing to the shadow repository. If this pull
1018 Returns qualified url pointing to the shadow repository. If this pull
1017 request is closed there is no shadow repository and ``None`` will be
1019 request is closed there is no shadow repository and ``None`` will be
1018 returned.
1020 returned.
1019 """
1021 """
1020 if pull_request.is_closed():
1022 if pull_request.is_closed():
1021 return None
1023 return None
1022 else:
1024 else:
1023 pr_url = urllib.unquote(self.get_url(pull_request))
1025 pr_url = urllib.unquote(self.get_url(pull_request))
1024 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1026 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1025
1027
1026 def notify_reviewers(self, pull_request, reviewers_ids):
1028 def notify_reviewers(self, pull_request, reviewers_ids):
1027 # notification to reviewers
1029 # notification to reviewers
1028 if not reviewers_ids:
1030 if not reviewers_ids:
1029 return
1031 return
1030
1032
1031 pull_request_obj = pull_request
1033 pull_request_obj = pull_request
1032 # get the current participants of this pull request
1034 # get the current participants of this pull request
1033 recipients = reviewers_ids
1035 recipients = reviewers_ids
1034 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1036 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1035
1037
1036 pr_source_repo = pull_request_obj.source_repo
1038 pr_source_repo = pull_request_obj.source_repo
1037 pr_target_repo = pull_request_obj.target_repo
1039 pr_target_repo = pull_request_obj.target_repo
1038
1040
1039 pr_url = h.route_url('pullrequest_show',
1041 pr_url = h.route_url('pullrequest_show',
1040 repo_name=pr_target_repo.repo_name,
1042 repo_name=pr_target_repo.repo_name,
1041 pull_request_id=pull_request_obj.pull_request_id,)
1043 pull_request_id=pull_request_obj.pull_request_id,)
1042
1044
1043 # set some variables for email notification
1045 # set some variables for email notification
1044 pr_target_repo_url = h.route_url(
1046 pr_target_repo_url = h.route_url(
1045 'repo_summary', repo_name=pr_target_repo.repo_name)
1047 'repo_summary', repo_name=pr_target_repo.repo_name)
1046
1048
1047 pr_source_repo_url = h.route_url(
1049 pr_source_repo_url = h.route_url(
1048 'repo_summary', repo_name=pr_source_repo.repo_name)
1050 'repo_summary', repo_name=pr_source_repo.repo_name)
1049
1051
1050 # pull request specifics
1052 # pull request specifics
1051 pull_request_commits = [
1053 pull_request_commits = [
1052 (x.raw_id, x.message)
1054 (x.raw_id, x.message)
1053 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1055 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1054
1056
1055 kwargs = {
1057 kwargs = {
1056 'user': pull_request.author,
1058 'user': pull_request.author,
1057 'pull_request': pull_request_obj,
1059 'pull_request': pull_request_obj,
1058 'pull_request_commits': pull_request_commits,
1060 'pull_request_commits': pull_request_commits,
1059
1061
1060 'pull_request_target_repo': pr_target_repo,
1062 'pull_request_target_repo': pr_target_repo,
1061 'pull_request_target_repo_url': pr_target_repo_url,
1063 'pull_request_target_repo_url': pr_target_repo_url,
1062
1064
1063 'pull_request_source_repo': pr_source_repo,
1065 'pull_request_source_repo': pr_source_repo,
1064 'pull_request_source_repo_url': pr_source_repo_url,
1066 'pull_request_source_repo_url': pr_source_repo_url,
1065
1067
1066 'pull_request_url': pr_url,
1068 'pull_request_url': pr_url,
1067 }
1069 }
1068
1070
1069 # pre-generate the subject for notification itself
1071 # pre-generate the subject for notification itself
1070 (subject,
1072 (subject,
1071 _h, _e, # we don't care about those
1073 _h, _e, # we don't care about those
1072 body_plaintext) = EmailNotificationModel().render_email(
1074 body_plaintext) = EmailNotificationModel().render_email(
1073 notification_type, **kwargs)
1075 notification_type, **kwargs)
1074
1076
1075 # create notification objects, and emails
1077 # create notification objects, and emails
1076 NotificationModel().create(
1078 NotificationModel().create(
1077 created_by=pull_request.author,
1079 created_by=pull_request.author,
1078 notification_subject=subject,
1080 notification_subject=subject,
1079 notification_body=body_plaintext,
1081 notification_body=body_plaintext,
1080 notification_type=notification_type,
1082 notification_type=notification_type,
1081 recipients=recipients,
1083 recipients=recipients,
1082 email_kwargs=kwargs,
1084 email_kwargs=kwargs,
1083 )
1085 )
1084
1086
1085 def delete(self, pull_request, user):
1087 def delete(self, pull_request, user):
1086 pull_request = self.__get_pull_request(pull_request)
1088 pull_request = self.__get_pull_request(pull_request)
1087 old_data = pull_request.get_api_data(with_merge_state=False)
1089 old_data = pull_request.get_api_data(with_merge_state=False)
1088 self._cleanup_merge_workspace(pull_request)
1090 self._cleanup_merge_workspace(pull_request)
1089 self._log_audit_action(
1091 self._log_audit_action(
1090 'repo.pull_request.delete', {'old_data': old_data},
1092 'repo.pull_request.delete', {'old_data': old_data},
1091 user, pull_request)
1093 user, pull_request)
1092 Session().delete(pull_request)
1094 Session().delete(pull_request)
1093
1095
1094 def close_pull_request(self, pull_request, user):
1096 def close_pull_request(self, pull_request, user):
1095 pull_request = self.__get_pull_request(pull_request)
1097 pull_request = self.__get_pull_request(pull_request)
1096 self._cleanup_merge_workspace(pull_request)
1098 self._cleanup_merge_workspace(pull_request)
1097 pull_request.status = PullRequest.STATUS_CLOSED
1099 pull_request.status = PullRequest.STATUS_CLOSED
1098 pull_request.updated_on = datetime.datetime.now()
1100 pull_request.updated_on = datetime.datetime.now()
1099 Session().add(pull_request)
1101 Session().add(pull_request)
1100 self._trigger_pull_request_hook(
1102 self._trigger_pull_request_hook(
1101 pull_request, pull_request.author, 'close')
1103 pull_request, pull_request.author, 'close')
1102
1104
1103 pr_data = pull_request.get_api_data(with_merge_state=False)
1105 pr_data = pull_request.get_api_data(with_merge_state=False)
1104 self._log_audit_action(
1106 self._log_audit_action(
1105 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1107 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1106
1108
1107 def close_pull_request_with_comment(
1109 def close_pull_request_with_comment(
1108 self, pull_request, user, repo, message=None):
1110 self, pull_request, user, repo, message=None):
1109
1111
1110 pull_request_review_status = pull_request.calculated_review_status()
1112 pull_request_review_status = pull_request.calculated_review_status()
1111
1113
1112 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1114 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1113 # approved only if we have voting consent
1115 # approved only if we have voting consent
1114 status = ChangesetStatus.STATUS_APPROVED
1116 status = ChangesetStatus.STATUS_APPROVED
1115 else:
1117 else:
1116 status = ChangesetStatus.STATUS_REJECTED
1118 status = ChangesetStatus.STATUS_REJECTED
1117 status_lbl = ChangesetStatus.get_status_lbl(status)
1119 status_lbl = ChangesetStatus.get_status_lbl(status)
1118
1120
1119 default_message = (
1121 default_message = (
1120 'Closing with status change {transition_icon} {status}.'
1122 'Closing with status change {transition_icon} {status}.'
1121 ).format(transition_icon='>', status=status_lbl)
1123 ).format(transition_icon='>', status=status_lbl)
1122 text = message or default_message
1124 text = message or default_message
1123
1125
1124 # create a comment, and link it to new status
1126 # create a comment, and link it to new status
1125 comment = CommentsModel().create(
1127 comment = CommentsModel().create(
1126 text=text,
1128 text=text,
1127 repo=repo.repo_id,
1129 repo=repo.repo_id,
1128 user=user.user_id,
1130 user=user.user_id,
1129 pull_request=pull_request.pull_request_id,
1131 pull_request=pull_request.pull_request_id,
1130 status_change=status_lbl,
1132 status_change=status_lbl,
1131 status_change_type=status,
1133 status_change_type=status,
1132 closing_pr=True
1134 closing_pr=True
1133 )
1135 )
1134
1136
1135 # calculate old status before we change it
1137 # calculate old status before we change it
1136 old_calculated_status = pull_request.calculated_review_status()
1138 old_calculated_status = pull_request.calculated_review_status()
1137 ChangesetStatusModel().set_status(
1139 ChangesetStatusModel().set_status(
1138 repo.repo_id,
1140 repo.repo_id,
1139 status,
1141 status,
1140 user.user_id,
1142 user.user_id,
1141 comment=comment,
1143 comment=comment,
1142 pull_request=pull_request.pull_request_id
1144 pull_request=pull_request.pull_request_id
1143 )
1145 )
1144
1146
1145 Session().flush()
1147 Session().flush()
1146 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1148 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1147 # we now calculate the status of pull request again, and based on that
1149 # we now calculate the status of pull request again, and based on that
1148 # calculation trigger status change. This might happen in cases
1150 # calculation trigger status change. This might happen in cases
1149 # that non-reviewer admin closes a pr, which means his vote doesn't
1151 # that non-reviewer admin closes a pr, which means his vote doesn't
1150 # change the status, while if he's a reviewer this might change it.
1152 # change the status, while if he's a reviewer this might change it.
1151 calculated_status = pull_request.calculated_review_status()
1153 calculated_status = pull_request.calculated_review_status()
1152 if old_calculated_status != calculated_status:
1154 if old_calculated_status != calculated_status:
1153 self._trigger_pull_request_hook(
1155 self._trigger_pull_request_hook(
1154 pull_request, user, 'review_status_change')
1156 pull_request, user, 'review_status_change')
1155
1157
1156 # finally close the PR
1158 # finally close the PR
1157 PullRequestModel().close_pull_request(
1159 PullRequestModel().close_pull_request(
1158 pull_request.pull_request_id, user)
1160 pull_request.pull_request_id, user)
1159
1161
1160 return comment, status
1162 return comment, status
1161
1163
1162 def merge_status(self, pull_request, translator=None):
1164 def merge_status(self, pull_request, translator=None):
1163 _ = translator or get_current_request().translate
1165 _ = translator or get_current_request().translate
1164
1166
1165 if not self._is_merge_enabled(pull_request):
1167 if not self._is_merge_enabled(pull_request):
1166 return False, _('Server-side pull request merging is disabled.')
1168 return False, _('Server-side pull request merging is disabled.')
1167 if pull_request.is_closed():
1169 if pull_request.is_closed():
1168 return False, _('This pull request is closed.')
1170 return False, _('This pull request is closed.')
1169 merge_possible, msg = self._check_repo_requirements(
1171 merge_possible, msg = self._check_repo_requirements(
1170 target=pull_request.target_repo, source=pull_request.source_repo,
1172 target=pull_request.target_repo, source=pull_request.source_repo,
1171 translator=_)
1173 translator=_)
1172 if not merge_possible:
1174 if not merge_possible:
1173 return merge_possible, msg
1175 return merge_possible, msg
1174
1176
1175 try:
1177 try:
1176 resp = self._try_merge(pull_request)
1178 resp = self._try_merge(pull_request)
1177 log.debug("Merge response: %s", resp)
1179 log.debug("Merge response: %s", resp)
1178 status = resp.possible, self.merge_status_message(
1180 status = resp.possible, self.merge_status_message(
1179 resp.failure_reason)
1181 resp.failure_reason)
1180 except NotImplementedError:
1182 except NotImplementedError:
1181 status = False, _('Pull request merging is not supported.')
1183 status = False, _('Pull request merging is not supported.')
1182
1184
1183 return status
1185 return status
1184
1186
1185 def _check_repo_requirements(self, target, source, translator):
1187 def _check_repo_requirements(self, target, source, translator):
1186 """
1188 """
1187 Check if `target` and `source` have compatible requirements.
1189 Check if `target` and `source` have compatible requirements.
1188
1190
1189 Currently this is just checking for largefiles.
1191 Currently this is just checking for largefiles.
1190 """
1192 """
1191 _ = translator
1193 _ = translator
1192 target_has_largefiles = self._has_largefiles(target)
1194 target_has_largefiles = self._has_largefiles(target)
1193 source_has_largefiles = self._has_largefiles(source)
1195 source_has_largefiles = self._has_largefiles(source)
1194 merge_possible = True
1196 merge_possible = True
1195 message = u''
1197 message = u''
1196
1198
1197 if target_has_largefiles != source_has_largefiles:
1199 if target_has_largefiles != source_has_largefiles:
1198 merge_possible = False
1200 merge_possible = False
1199 if source_has_largefiles:
1201 if source_has_largefiles:
1200 message = _(
1202 message = _(
1201 'Target repository large files support is disabled.')
1203 'Target repository large files support is disabled.')
1202 else:
1204 else:
1203 message = _(
1205 message = _(
1204 'Source repository large files support is disabled.')
1206 'Source repository large files support is disabled.')
1205
1207
1206 return merge_possible, message
1208 return merge_possible, message
1207
1209
1208 def _has_largefiles(self, repo):
1210 def _has_largefiles(self, repo):
1209 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1211 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1210 'extensions', 'largefiles')
1212 'extensions', 'largefiles')
1211 return largefiles_ui and largefiles_ui[0].active
1213 return largefiles_ui and largefiles_ui[0].active
1212
1214
1213 def _try_merge(self, pull_request):
1215 def _try_merge(self, pull_request):
1214 """
1216 """
1215 Try to merge the pull request and return the merge status.
1217 Try to merge the pull request and return the merge status.
1216 """
1218 """
1217 log.debug(
1219 log.debug(
1218 "Trying out if the pull request %s can be merged.",
1220 "Trying out if the pull request %s can be merged.",
1219 pull_request.pull_request_id)
1221 pull_request.pull_request_id)
1220 target_vcs = pull_request.target_repo.scm_instance()
1222 target_vcs = pull_request.target_repo.scm_instance()
1221
1223
1222 # Refresh the target reference.
1224 # Refresh the target reference.
1223 try:
1225 try:
1224 target_ref = self._refresh_reference(
1226 target_ref = self._refresh_reference(
1225 pull_request.target_ref_parts, target_vcs)
1227 pull_request.target_ref_parts, target_vcs)
1226 except CommitDoesNotExistError:
1228 except CommitDoesNotExistError:
1227 merge_state = MergeResponse(
1229 merge_state = MergeResponse(
1228 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1230 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1229 return merge_state
1231 return merge_state
1230
1232
1231 target_locked = pull_request.target_repo.locked
1233 target_locked = pull_request.target_repo.locked
1232 if target_locked and target_locked[0]:
1234 if target_locked and target_locked[0]:
1233 log.debug("The target repository is locked.")
1235 log.debug("The target repository is locked.")
1234 merge_state = MergeResponse(
1236 merge_state = MergeResponse(
1235 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1237 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1236 elif self._needs_merge_state_refresh(pull_request, target_ref):
1238 elif self._needs_merge_state_refresh(pull_request, target_ref):
1237 log.debug("Refreshing the merge status of the repository.")
1239 log.debug("Refreshing the merge status of the repository.")
1238 merge_state = self._refresh_merge_state(
1240 merge_state = self._refresh_merge_state(
1239 pull_request, target_vcs, target_ref)
1241 pull_request, target_vcs, target_ref)
1240 else:
1242 else:
1241 possible = pull_request.\
1243 possible = pull_request.\
1242 last_merge_status == MergeFailureReason.NONE
1244 last_merge_status == MergeFailureReason.NONE
1243 merge_state = MergeResponse(
1245 merge_state = MergeResponse(
1244 possible, False, None, pull_request.last_merge_status)
1246 possible, False, None, pull_request.last_merge_status)
1245
1247
1246 return merge_state
1248 return merge_state
1247
1249
1248 def _refresh_reference(self, reference, vcs_repository):
1250 def _refresh_reference(self, reference, vcs_repository):
1249 if reference.type in ('branch', 'book'):
1251 if reference.type in ('branch', 'book'):
1250 name_or_id = reference.name
1252 name_or_id = reference.name
1251 else:
1253 else:
1252 name_or_id = reference.commit_id
1254 name_or_id = reference.commit_id
1253 refreshed_commit = vcs_repository.get_commit(name_or_id)
1255 refreshed_commit = vcs_repository.get_commit(name_or_id)
1254 refreshed_reference = Reference(
1256 refreshed_reference = Reference(
1255 reference.type, reference.name, refreshed_commit.raw_id)
1257 reference.type, reference.name, refreshed_commit.raw_id)
1256 return refreshed_reference
1258 return refreshed_reference
1257
1259
1258 def _needs_merge_state_refresh(self, pull_request, target_reference):
1260 def _needs_merge_state_refresh(self, pull_request, target_reference):
1259 return not(
1261 return not(
1260 pull_request.revisions and
1262 pull_request.revisions and
1261 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1263 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1262 target_reference.commit_id == pull_request._last_merge_target_rev)
1264 target_reference.commit_id == pull_request._last_merge_target_rev)
1263
1265
1264 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1266 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1265 workspace_id = self._workspace_id(pull_request)
1267 workspace_id = self._workspace_id(pull_request)
1266 source_vcs = pull_request.source_repo.scm_instance()
1268 source_vcs = pull_request.source_repo.scm_instance()
1267 use_rebase = self._use_rebase_for_merging(pull_request)
1269 use_rebase = self._use_rebase_for_merging(pull_request)
1268 close_branch = self._close_branch_before_merging(pull_request)
1270 close_branch = self._close_branch_before_merging(pull_request)
1269 merge_state = target_vcs.merge(
1271 merge_state = target_vcs.merge(
1270 target_reference, source_vcs, pull_request.source_ref_parts,
1272 target_reference, source_vcs, pull_request.source_ref_parts,
1271 workspace_id, dry_run=True, use_rebase=use_rebase,
1273 workspace_id, dry_run=True, use_rebase=use_rebase,
1272 close_branch=close_branch)
1274 close_branch=close_branch)
1273
1275
1274 # Do not store the response if there was an unknown error.
1276 # Do not store the response if there was an unknown error.
1275 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1277 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1276 pull_request._last_merge_source_rev = \
1278 pull_request._last_merge_source_rev = \
1277 pull_request.source_ref_parts.commit_id
1279 pull_request.source_ref_parts.commit_id
1278 pull_request._last_merge_target_rev = target_reference.commit_id
1280 pull_request._last_merge_target_rev = target_reference.commit_id
1279 pull_request.last_merge_status = merge_state.failure_reason
1281 pull_request.last_merge_status = merge_state.failure_reason
1280 pull_request.shadow_merge_ref = merge_state.merge_ref
1282 pull_request.shadow_merge_ref = merge_state.merge_ref
1281 Session().add(pull_request)
1283 Session().add(pull_request)
1282 Session().commit()
1284 Session().commit()
1283
1285
1284 return merge_state
1286 return merge_state
1285
1287
1286 def _workspace_id(self, pull_request):
1288 def _workspace_id(self, pull_request):
1287 workspace_id = 'pr-%s' % pull_request.pull_request_id
1289 workspace_id = 'pr-%s' % pull_request.pull_request_id
1288 return workspace_id
1290 return workspace_id
1289
1291
1290 def merge_status_message(self, status_code):
1292 def merge_status_message(self, status_code):
1291 """
1293 """
1292 Return a human friendly error message for the given merge status code.
1294 Return a human friendly error message for the given merge status code.
1293 """
1295 """
1294 return self.MERGE_STATUS_MESSAGES[status_code]
1296 return self.MERGE_STATUS_MESSAGES[status_code]
1295
1297
1296 def generate_repo_data(self, repo, commit_id=None, branch=None,
1298 def generate_repo_data(self, repo, commit_id=None, branch=None,
1297 bookmark=None, translator=None):
1299 bookmark=None, translator=None):
1298
1300
1299 all_refs, selected_ref = \
1301 all_refs, selected_ref = \
1300 self._get_repo_pullrequest_sources(
1302 self._get_repo_pullrequest_sources(
1301 repo.scm_instance(), commit_id=commit_id,
1303 repo.scm_instance(), commit_id=commit_id,
1302 branch=branch, bookmark=bookmark, translator=translator)
1304 branch=branch, bookmark=bookmark, translator=translator)
1303
1305
1304 refs_select2 = []
1306 refs_select2 = []
1305 for element in all_refs:
1307 for element in all_refs:
1306 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1308 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1307 refs_select2.append({'text': element[1], 'children': children})
1309 refs_select2.append({'text': element[1], 'children': children})
1308
1310
1309 return {
1311 return {
1310 'user': {
1312 'user': {
1311 'user_id': repo.user.user_id,
1313 'user_id': repo.user.user_id,
1312 'username': repo.user.username,
1314 'username': repo.user.username,
1313 'firstname': repo.user.first_name,
1315 'firstname': repo.user.first_name,
1314 'lastname': repo.user.last_name,
1316 'lastname': repo.user.last_name,
1315 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1317 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1316 },
1318 },
1317 'description': h.chop_at_smart(repo.description_safe, '\n'),
1319 'description': h.chop_at_smart(repo.description_safe, '\n'),
1318 'refs': {
1320 'refs': {
1319 'all_refs': all_refs,
1321 'all_refs': all_refs,
1320 'selected_ref': selected_ref,
1322 'selected_ref': selected_ref,
1321 'select2_refs': refs_select2
1323 'select2_refs': refs_select2
1322 }
1324 }
1323 }
1325 }
1324
1326
1325 def generate_pullrequest_title(self, source, source_ref, target):
1327 def generate_pullrequest_title(self, source, source_ref, target):
1326 return u'{source}#{at_ref} to {target}'.format(
1328 return u'{source}#{at_ref} to {target}'.format(
1327 source=source,
1329 source=source,
1328 at_ref=source_ref,
1330 at_ref=source_ref,
1329 target=target,
1331 target=target,
1330 )
1332 )
1331
1333
1332 def _cleanup_merge_workspace(self, pull_request):
1334 def _cleanup_merge_workspace(self, pull_request):
1333 # Merging related cleanup
1335 # Merging related cleanup
1334 target_scm = pull_request.target_repo.scm_instance()
1336 target_scm = pull_request.target_repo.scm_instance()
1335 workspace_id = 'pr-%s' % pull_request.pull_request_id
1337 workspace_id = 'pr-%s' % pull_request.pull_request_id
1336
1338
1337 try:
1339 try:
1338 target_scm.cleanup_merge_workspace(workspace_id)
1340 target_scm.cleanup_merge_workspace(workspace_id)
1339 except NotImplementedError:
1341 except NotImplementedError:
1340 pass
1342 pass
1341
1343
1342 def _get_repo_pullrequest_sources(
1344 def _get_repo_pullrequest_sources(
1343 self, repo, commit_id=None, branch=None, bookmark=None,
1345 self, repo, commit_id=None, branch=None, bookmark=None,
1344 translator=None):
1346 translator=None):
1345 """
1347 """
1346 Return a structure with repo's interesting commits, suitable for
1348 Return a structure with repo's interesting commits, suitable for
1347 the selectors in pullrequest controller
1349 the selectors in pullrequest controller
1348
1350
1349 :param commit_id: a commit that must be in the list somehow
1351 :param commit_id: a commit that must be in the list somehow
1350 and selected by default
1352 and selected by default
1351 :param branch: a branch that must be in the list and selected
1353 :param branch: a branch that must be in the list and selected
1352 by default - even if closed
1354 by default - even if closed
1353 :param bookmark: a bookmark that must be in the list and selected
1355 :param bookmark: a bookmark that must be in the list and selected
1354 """
1356 """
1355 _ = translator or get_current_request().translate
1357 _ = translator or get_current_request().translate
1356
1358
1357 commit_id = safe_str(commit_id) if commit_id else None
1359 commit_id = safe_str(commit_id) if commit_id else None
1358 branch = safe_str(branch) if branch else None
1360 branch = safe_str(branch) if branch else None
1359 bookmark = safe_str(bookmark) if bookmark else None
1361 bookmark = safe_str(bookmark) if bookmark else None
1360
1362
1361 selected = None
1363 selected = None
1362
1364
1363 # order matters: first source that has commit_id in it will be selected
1365 # order matters: first source that has commit_id in it will be selected
1364 sources = []
1366 sources = []
1365 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1367 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1366 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1368 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1367
1369
1368 if commit_id:
1370 if commit_id:
1369 ref_commit = (h.short_id(commit_id), commit_id)
1371 ref_commit = (h.short_id(commit_id), commit_id)
1370 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1372 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1371
1373
1372 sources.append(
1374 sources.append(
1373 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1375 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1374 )
1376 )
1375
1377
1376 groups = []
1378 groups = []
1377 for group_key, ref_list, group_name, match in sources:
1379 for group_key, ref_list, group_name, match in sources:
1378 group_refs = []
1380 group_refs = []
1379 for ref_name, ref_id in ref_list:
1381 for ref_name, ref_id in ref_list:
1380 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1382 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1381 group_refs.append((ref_key, ref_name))
1383 group_refs.append((ref_key, ref_name))
1382
1384
1383 if not selected:
1385 if not selected:
1384 if set([commit_id, match]) & set([ref_id, ref_name]):
1386 if set([commit_id, match]) & set([ref_id, ref_name]):
1385 selected = ref_key
1387 selected = ref_key
1386
1388
1387 if group_refs:
1389 if group_refs:
1388 groups.append((group_refs, group_name))
1390 groups.append((group_refs, group_name))
1389
1391
1390 if not selected:
1392 if not selected:
1391 ref = commit_id or branch or bookmark
1393 ref = commit_id or branch or bookmark
1392 if ref:
1394 if ref:
1393 raise CommitDoesNotExistError(
1395 raise CommitDoesNotExistError(
1394 'No commit refs could be found matching: %s' % ref)
1396 'No commit refs could be found matching: %s' % ref)
1395 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1397 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1396 selected = 'branch:%s:%s' % (
1398 selected = 'branch:%s:%s' % (
1397 repo.DEFAULT_BRANCH_NAME,
1399 repo.DEFAULT_BRANCH_NAME,
1398 repo.branches[repo.DEFAULT_BRANCH_NAME]
1400 repo.branches[repo.DEFAULT_BRANCH_NAME]
1399 )
1401 )
1400 elif repo.commit_ids:
1402 elif repo.commit_ids:
1401 rev = repo.commit_ids[0]
1403 rev = repo.commit_ids[0]
1402 selected = 'rev:%s:%s' % (rev, rev)
1404 selected = 'rev:%s:%s' % (rev, rev)
1403 else:
1405 else:
1404 raise EmptyRepositoryError()
1406 raise EmptyRepositoryError()
1405 return groups, selected
1407 return groups, selected
1406
1408
1407 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1409 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1408 return self._get_diff_from_pr_or_version(
1410 return self._get_diff_from_pr_or_version(
1409 source_repo, source_ref_id, target_ref_id, context=context)
1411 source_repo, source_ref_id, target_ref_id, context=context)
1410
1412
1411 def _get_diff_from_pr_or_version(
1413 def _get_diff_from_pr_or_version(
1412 self, source_repo, source_ref_id, target_ref_id, context):
1414 self, source_repo, source_ref_id, target_ref_id, context):
1413 target_commit = source_repo.get_commit(
1415 target_commit = source_repo.get_commit(
1414 commit_id=safe_str(target_ref_id))
1416 commit_id=safe_str(target_ref_id))
1415 source_commit = source_repo.get_commit(
1417 source_commit = source_repo.get_commit(
1416 commit_id=safe_str(source_ref_id))
1418 commit_id=safe_str(source_ref_id))
1417 if isinstance(source_repo, Repository):
1419 if isinstance(source_repo, Repository):
1418 vcs_repo = source_repo.scm_instance()
1420 vcs_repo = source_repo.scm_instance()
1419 else:
1421 else:
1420 vcs_repo = source_repo
1422 vcs_repo = source_repo
1421
1423
1422 # TODO: johbo: In the context of an update, we cannot reach
1424 # TODO: johbo: In the context of an update, we cannot reach
1423 # the old commit anymore with our normal mechanisms. It needs
1425 # the old commit anymore with our normal mechanisms. It needs
1424 # some sort of special support in the vcs layer to avoid this
1426 # some sort of special support in the vcs layer to avoid this
1425 # workaround.
1427 # workaround.
1426 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1428 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1427 vcs_repo.alias == 'git'):
1429 vcs_repo.alias == 'git'):
1428 source_commit.raw_id = safe_str(source_ref_id)
1430 source_commit.raw_id = safe_str(source_ref_id)
1429
1431
1430 log.debug('calculating diff between '
1432 log.debug('calculating diff between '
1431 'source_ref:%s and target_ref:%s for repo `%s`',
1433 'source_ref:%s and target_ref:%s for repo `%s`',
1432 target_ref_id, source_ref_id,
1434 target_ref_id, source_ref_id,
1433 safe_unicode(vcs_repo.path))
1435 safe_unicode(vcs_repo.path))
1434
1436
1435 vcs_diff = vcs_repo.get_diff(
1437 vcs_diff = vcs_repo.get_diff(
1436 commit1=target_commit, commit2=source_commit, context=context)
1438 commit1=target_commit, commit2=source_commit, context=context)
1437 return vcs_diff
1439 return vcs_diff
1438
1440
1439 def _is_merge_enabled(self, pull_request):
1441 def _is_merge_enabled(self, pull_request):
1440 return self._get_general_setting(
1442 return self._get_general_setting(
1441 pull_request, 'rhodecode_pr_merge_enabled')
1443 pull_request, 'rhodecode_pr_merge_enabled')
1442
1444
1443 def _use_rebase_for_merging(self, pull_request):
1445 def _use_rebase_for_merging(self, pull_request):
1444 repo_type = pull_request.target_repo.repo_type
1446 repo_type = pull_request.target_repo.repo_type
1445 if repo_type == 'hg':
1447 if repo_type == 'hg':
1446 return self._get_general_setting(
1448 return self._get_general_setting(
1447 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1449 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1448 elif repo_type == 'git':
1450 elif repo_type == 'git':
1449 return self._get_general_setting(
1451 return self._get_general_setting(
1450 pull_request, 'rhodecode_git_use_rebase_for_merging')
1452 pull_request, 'rhodecode_git_use_rebase_for_merging')
1451
1453
1452 return False
1454 return False
1453
1455
1454 def _close_branch_before_merging(self, pull_request):
1456 def _close_branch_before_merging(self, pull_request):
1455 repo_type = pull_request.target_repo.repo_type
1457 repo_type = pull_request.target_repo.repo_type
1456 if repo_type == 'hg':
1458 if repo_type == 'hg':
1457 return self._get_general_setting(
1459 return self._get_general_setting(
1458 pull_request, 'rhodecode_hg_close_branch_before_merging')
1460 pull_request, 'rhodecode_hg_close_branch_before_merging')
1459 elif repo_type == 'git':
1461 elif repo_type == 'git':
1460 return self._get_general_setting(
1462 return self._get_general_setting(
1461 pull_request, 'rhodecode_git_close_branch_before_merging')
1463 pull_request, 'rhodecode_git_close_branch_before_merging')
1462
1464
1463 return False
1465 return False
1464
1466
1465 def _get_general_setting(self, pull_request, settings_key, default=False):
1467 def _get_general_setting(self, pull_request, settings_key, default=False):
1466 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1468 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1467 settings = settings_model.get_general_settings()
1469 settings = settings_model.get_general_settings()
1468 return settings.get(settings_key, default)
1470 return settings.get(settings_key, default)
1469
1471
1470 def _log_audit_action(self, action, action_data, user, pull_request):
1472 def _log_audit_action(self, action, action_data, user, pull_request):
1471 audit_logger.store(
1473 audit_logger.store(
1472 action=action,
1474 action=action,
1473 action_data=action_data,
1475 action_data=action_data,
1474 user=user,
1476 user=user,
1475 repo=pull_request.target_repo)
1477 repo=pull_request.target_repo)
1476
1478
1477 def get_reviewer_functions(self):
1479 def get_reviewer_functions(self):
1478 """
1480 """
1479 Fetches functions for validation and fetching default reviewers.
1481 Fetches functions for validation and fetching default reviewers.
1480 If available we use the EE package, else we fallback to CE
1482 If available we use the EE package, else we fallback to CE
1481 package functions
1483 package functions
1482 """
1484 """
1483 try:
1485 try:
1484 from rc_reviewers.utils import get_default_reviewers_data
1486 from rc_reviewers.utils import get_default_reviewers_data
1485 from rc_reviewers.utils import validate_default_reviewers
1487 from rc_reviewers.utils import validate_default_reviewers
1486 except ImportError:
1488 except ImportError:
1487 from rhodecode.apps.repository.utils import \
1489 from rhodecode.apps.repository.utils import \
1488 get_default_reviewers_data
1490 get_default_reviewers_data
1489 from rhodecode.apps.repository.utils import \
1491 from rhodecode.apps.repository.utils import \
1490 validate_default_reviewers
1492 validate_default_reviewers
1491
1493
1492 return get_default_reviewers_data, validate_default_reviewers
1494 return get_default_reviewers_data, validate_default_reviewers
1493
1495
1494
1496
1495 class MergeCheck(object):
1497 class MergeCheck(object):
1496 """
1498 """
1497 Perform Merge Checks and returns a check object which stores information
1499 Perform Merge Checks and returns a check object which stores information
1498 about merge errors, and merge conditions
1500 about merge errors, and merge conditions
1499 """
1501 """
1500 TODO_CHECK = 'todo'
1502 TODO_CHECK = 'todo'
1501 PERM_CHECK = 'perm'
1503 PERM_CHECK = 'perm'
1502 REVIEW_CHECK = 'review'
1504 REVIEW_CHECK = 'review'
1503 MERGE_CHECK = 'merge'
1505 MERGE_CHECK = 'merge'
1504
1506
1505 def __init__(self):
1507 def __init__(self):
1506 self.review_status = None
1508 self.review_status = None
1507 self.merge_possible = None
1509 self.merge_possible = None
1508 self.merge_msg = ''
1510 self.merge_msg = ''
1509 self.failed = None
1511 self.failed = None
1510 self.errors = []
1512 self.errors = []
1511 self.error_details = OrderedDict()
1513 self.error_details = OrderedDict()
1512
1514
1513 def push_error(self, error_type, message, error_key, details):
1515 def push_error(self, error_type, message, error_key, details):
1514 self.failed = True
1516 self.failed = True
1515 self.errors.append([error_type, message])
1517 self.errors.append([error_type, message])
1516 self.error_details[error_key] = dict(
1518 self.error_details[error_key] = dict(
1517 details=details,
1519 details=details,
1518 error_type=error_type,
1520 error_type=error_type,
1519 message=message
1521 message=message
1520 )
1522 )
1521
1523
1522 @classmethod
1524 @classmethod
1523 def validate(cls, pull_request, user, translator, fail_early=False):
1525 def validate(cls, pull_request, user, translator, fail_early=False):
1524 _ = translator
1526 _ = translator
1525 merge_check = cls()
1527 merge_check = cls()
1526
1528
1527 # permissions to merge
1529 # permissions to merge
1528 user_allowed_to_merge = PullRequestModel().check_user_merge(
1530 user_allowed_to_merge = PullRequestModel().check_user_merge(
1529 pull_request, user)
1531 pull_request, user)
1530 if not user_allowed_to_merge:
1532 if not user_allowed_to_merge:
1531 log.debug("MergeCheck: cannot merge, approval is pending.")
1533 log.debug("MergeCheck: cannot merge, approval is pending.")
1532
1534
1533 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1535 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1534 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1536 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1535 if fail_early:
1537 if fail_early:
1536 return merge_check
1538 return merge_check
1537
1539
1538 # review status, must be always present
1540 # review status, must be always present
1539 review_status = pull_request.calculated_review_status()
1541 review_status = pull_request.calculated_review_status()
1540 merge_check.review_status = review_status
1542 merge_check.review_status = review_status
1541
1543
1542 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1544 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1543 if not status_approved:
1545 if not status_approved:
1544 log.debug("MergeCheck: cannot merge, approval is pending.")
1546 log.debug("MergeCheck: cannot merge, approval is pending.")
1545
1547
1546 msg = _('Pull request reviewer approval is pending.')
1548 msg = _('Pull request reviewer approval is pending.')
1547
1549
1548 merge_check.push_error(
1550 merge_check.push_error(
1549 'warning', msg, cls.REVIEW_CHECK, review_status)
1551 'warning', msg, cls.REVIEW_CHECK, review_status)
1550
1552
1551 if fail_early:
1553 if fail_early:
1552 return merge_check
1554 return merge_check
1553
1555
1554 # left over TODOs
1556 # left over TODOs
1555 todos = CommentsModel().get_unresolved_todos(pull_request)
1557 todos = CommentsModel().get_unresolved_todos(pull_request)
1556 if todos:
1558 if todos:
1557 log.debug("MergeCheck: cannot merge, {} "
1559 log.debug("MergeCheck: cannot merge, {} "
1558 "unresolved todos left.".format(len(todos)))
1560 "unresolved todos left.".format(len(todos)))
1559
1561
1560 if len(todos) == 1:
1562 if len(todos) == 1:
1561 msg = _('Cannot merge, {} TODO still not resolved.').format(
1563 msg = _('Cannot merge, {} TODO still not resolved.').format(
1562 len(todos))
1564 len(todos))
1563 else:
1565 else:
1564 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1566 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1565 len(todos))
1567 len(todos))
1566
1568
1567 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1569 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1568
1570
1569 if fail_early:
1571 if fail_early:
1570 return merge_check
1572 return merge_check
1571
1573
1572 # merge possible
1574 # merge possible
1573 merge_status, msg = PullRequestModel().merge_status(
1575 merge_status, msg = PullRequestModel().merge_status(
1574 pull_request, translator=translator)
1576 pull_request, translator=translator)
1575 merge_check.merge_possible = merge_status
1577 merge_check.merge_possible = merge_status
1576 merge_check.merge_msg = msg
1578 merge_check.merge_msg = msg
1577 if not merge_status:
1579 if not merge_status:
1578 log.debug(
1580 log.debug(
1579 "MergeCheck: cannot merge, pull request merge not possible.")
1581 "MergeCheck: cannot merge, pull request merge not possible.")
1580 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1582 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1581
1583
1582 if fail_early:
1584 if fail_early:
1583 return merge_check
1585 return merge_check
1584
1586
1585 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1587 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1586 return merge_check
1588 return merge_check
1587
1589
1588 @classmethod
1590 @classmethod
1589 def get_merge_conditions(cls, pull_request, translator):
1591 def get_merge_conditions(cls, pull_request, translator):
1590 _ = translator
1592 _ = translator
1591 merge_details = {}
1593 merge_details = {}
1592
1594
1593 model = PullRequestModel()
1595 model = PullRequestModel()
1594 use_rebase = model._use_rebase_for_merging(pull_request)
1596 use_rebase = model._use_rebase_for_merging(pull_request)
1595
1597
1596 if use_rebase:
1598 if use_rebase:
1597 merge_details['merge_strategy'] = dict(
1599 merge_details['merge_strategy'] = dict(
1598 details={},
1600 details={},
1599 message=_('Merge strategy: rebase')
1601 message=_('Merge strategy: rebase')
1600 )
1602 )
1601 else:
1603 else:
1602 merge_details['merge_strategy'] = dict(
1604 merge_details['merge_strategy'] = dict(
1603 details={},
1605 details={},
1604 message=_('Merge strategy: explicit merge commit')
1606 message=_('Merge strategy: explicit merge commit')
1605 )
1607 )
1606
1608
1607 close_branch = model._close_branch_before_merging(pull_request)
1609 close_branch = model._close_branch_before_merging(pull_request)
1608 if close_branch:
1610 if close_branch:
1609 repo_type = pull_request.target_repo.repo_type
1611 repo_type = pull_request.target_repo.repo_type
1610 if repo_type == 'hg':
1612 if repo_type == 'hg':
1611 close_msg = _('Source branch will be closed after merge.')
1613 close_msg = _('Source branch will be closed after merge.')
1612 elif repo_type == 'git':
1614 elif repo_type == 'git':
1613 close_msg = _('Source branch will be deleted after merge.')
1615 close_msg = _('Source branch will be deleted after merge.')
1614
1616
1615 merge_details['close_branch'] = dict(
1617 merge_details['close_branch'] = dict(
1616 details={},
1618 details={},
1617 message=close_msg
1619 message=close_msg
1618 )
1620 )
1619
1621
1620 return merge_details
1622 return merge_details
1621
1623
1622 ChangeTuple = collections.namedtuple(
1624 ChangeTuple = collections.namedtuple(
1623 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1625 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1624
1626
1625 FileChangeTuple = collections.namedtuple(
1627 FileChangeTuple = collections.namedtuple(
1626 'FileChangeTuple', ['added', 'modified', 'removed'])
1628 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now