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