##// END OF EJS Templates
pull-requests: fix deletion of comments and make the test actually test this !...
marcink -
r1978:455f23f1 default
parent child Browse files
Show More
@@ -1,1110 +1,1135 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 202 pr_util.close()
203 203
204 204 response = self.app.post(
205 205 route_path('pullrequest_update',
206 206 repo_name=pull_request.target_repo.repo_name,
207 207 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 212 'csrf_token': csrf_token})
213 213
214 214 assert_session_flash(
215 215 response, u'Cannot update closed pull requests.',
216 216 category='error')
217 217
218 218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
219 219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
220 220
221 221 pull_request = pr_util.create_pull_request()
222 222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
223 223 Session().add(pull_request)
224 224 Session().commit()
225 225
226 226 pull_request_id = pull_request.pull_request_id
227 227
228 228 response = self.app.post(
229 229 route_path('pullrequest_update',
230 230 repo_name=pull_request.target_repo.repo_name,
231 231 pull_request_id=pull_request_id),
232 232 params={'update_commits': 'true',
233 233 'csrf_token': csrf_token})
234 234
235 235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
236 236 UpdateFailureReason.MISSING_SOURCE_REF]
237 237 assert_session_flash(response, expected_msg, category='error')
238 238
239 239 def test_missing_target_reference(self, pr_util, csrf_token):
240 240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
241 241 pull_request = pr_util.create_pull_request(
242 242 approved=True, mergeable=True)
243 243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
244 244 Session().add(pull_request)
245 245 Session().commit()
246 246
247 247 pull_request_id = pull_request.pull_request_id
248 248 pull_request_url = route_path(
249 249 'pullrequest_show',
250 250 repo_name=pull_request.target_repo.repo_name,
251 251 pull_request_id=pull_request_id)
252 252
253 253 response = self.app.get(pull_request_url)
254 254
255 255 assertr = AssertResponse(response)
256 256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
257 257 MergeFailureReason.MISSING_TARGET_REF]
258 258 assertr.element_contains(
259 259 'span[data-role="merge-message"]', str(expected_msg))
260 260
261 261 def test_comment_and_close_pull_request_custom_message_approved(
262 262 self, pr_util, csrf_token, xhr_header):
263 263
264 264 pull_request = pr_util.create_pull_request(approved=True)
265 265 pull_request_id = pull_request.pull_request_id
266 266 author = pull_request.user_id
267 267 repo = pull_request.target_repo.repo_id
268 268
269 269 self.app.post(
270 270 route_path('pullrequest_comment_create',
271 271 repo_name=pull_request.target_repo.scm_instance().name,
272 272 pull_request_id=pull_request_id),
273 273 params={
274 274 'close_pull_request': '1',
275 275 'text': 'Closing a PR',
276 276 'csrf_token': csrf_token},
277 277 extra_environ=xhr_header,)
278 278
279 279 journal = UserLog.query()\
280 280 .filter(UserLog.user_id == author)\
281 281 .filter(UserLog.repository_id == repo) \
282 282 .order_by('user_log_id') \
283 283 .all()
284 284 assert journal[-1].action == 'repo.pull_request.close'
285 285
286 286 pull_request = PullRequest.get(pull_request_id)
287 287 assert pull_request.is_closed()
288 288
289 289 status = ChangesetStatusModel().get_status(
290 290 pull_request.source_repo, pull_request=pull_request)
291 291 assert status == ChangesetStatus.STATUS_APPROVED
292 292 comments = ChangesetComment().query() \
293 293 .filter(ChangesetComment.pull_request == pull_request) \
294 294 .order_by(ChangesetComment.comment_id.asc())\
295 295 .all()
296 296 assert comments[-1].text == 'Closing a PR'
297 297
298 298 def test_comment_force_close_pull_request_rejected(
299 299 self, pr_util, csrf_token, xhr_header):
300 300 pull_request = pr_util.create_pull_request()
301 301 pull_request_id = pull_request.pull_request_id
302 302 PullRequestModel().update_reviewers(
303 303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
304 304 pull_request.author)
305 305 author = pull_request.user_id
306 306 repo = pull_request.target_repo.repo_id
307 307
308 308 self.app.post(
309 309 route_path('pullrequest_comment_create',
310 310 repo_name=pull_request.target_repo.scm_instance().name,
311 311 pull_request_id=pull_request_id),
312 312 params={
313 313 'close_pull_request': '1',
314 314 'csrf_token': csrf_token},
315 315 extra_environ=xhr_header)
316 316
317 317 pull_request = PullRequest.get(pull_request_id)
318 318
319 319 journal = UserLog.query()\
320 320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
321 321 .order_by('user_log_id') \
322 322 .all()
323 323 assert journal[-1].action == 'repo.pull_request.close'
324 324
325 325 # check only the latest status, not the review status
326 326 status = ChangesetStatusModel().get_status(
327 327 pull_request.source_repo, pull_request=pull_request)
328 328 assert status == ChangesetStatus.STATUS_REJECTED
329 329
330 330 def test_comment_and_close_pull_request(
331 331 self, pr_util, csrf_token, xhr_header):
332 332 pull_request = pr_util.create_pull_request()
333 333 pull_request_id = pull_request.pull_request_id
334 334
335 335 response = self.app.post(
336 336 route_path('pullrequest_comment_create',
337 337 repo_name=pull_request.target_repo.scm_instance().name,
338 338 pull_request_id=pull_request.pull_request_id),
339 339 params={
340 340 'close_pull_request': 'true',
341 341 'csrf_token': csrf_token},
342 342 extra_environ=xhr_header)
343 343
344 344 assert response.json
345 345
346 346 pull_request = PullRequest.get(pull_request_id)
347 347 assert pull_request.is_closed()
348 348
349 349 # check only the latest status, not the review status
350 350 status = ChangesetStatusModel().get_status(
351 351 pull_request.source_repo, pull_request=pull_request)
352 352 assert status == ChangesetStatus.STATUS_REJECTED
353 353
354 354 def test_create_pull_request(self, backend, csrf_token):
355 355 commits = [
356 356 {'message': 'ancestor'},
357 357 {'message': 'change'},
358 358 {'message': 'change2'},
359 359 ]
360 360 commit_ids = backend.create_master_repo(commits)
361 361 target = backend.create_repo(heads=['ancestor'])
362 362 source = backend.create_repo(heads=['change2'])
363 363
364 364 response = self.app.post(
365 365 route_path('pullrequest_create', repo_name=source.repo_name),
366 366 [
367 367 ('source_repo', source.repo_name),
368 368 ('source_ref', 'branch:default:' + commit_ids['change2']),
369 369 ('target_repo', target.repo_name),
370 370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
371 371 ('common_ancestor', commit_ids['ancestor']),
372 372 ('pullrequest_desc', 'Description'),
373 373 ('pullrequest_title', 'Title'),
374 374 ('__start__', 'review_members:sequence'),
375 375 ('__start__', 'reviewer:mapping'),
376 376 ('user_id', '1'),
377 377 ('__start__', 'reasons:sequence'),
378 378 ('reason', 'Some reason'),
379 379 ('__end__', 'reasons:sequence'),
380 380 ('mandatory', 'False'),
381 381 ('__end__', 'reviewer:mapping'),
382 382 ('__end__', 'review_members:sequence'),
383 383 ('__start__', 'revisions:sequence'),
384 384 ('revisions', commit_ids['change']),
385 385 ('revisions', commit_ids['change2']),
386 386 ('__end__', 'revisions:sequence'),
387 387 ('user', ''),
388 388 ('csrf_token', csrf_token),
389 389 ],
390 390 status=302)
391 391
392 392 location = response.headers['Location']
393 393 pull_request_id = location.rsplit('/', 1)[1]
394 394 assert pull_request_id != 'new'
395 395 pull_request = PullRequest.get(int(pull_request_id))
396 396
397 397 # check that we have now both revisions
398 398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
399 399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
400 400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
401 401 assert pull_request.target_ref == expected_target_ref
402 402
403 403 def test_reviewer_notifications(self, backend, csrf_token):
404 404 # We have to use the app.post for this test so it will create the
405 405 # notifications properly with the new PR
406 406 commits = [
407 407 {'message': 'ancestor',
408 408 'added': [FileNode('file_A', content='content_of_ancestor')]},
409 409 {'message': 'change',
410 410 'added': [FileNode('file_a', content='content_of_change')]},
411 411 {'message': 'change-child'},
412 412 {'message': 'ancestor-child', 'parents': ['ancestor'],
413 413 'added': [
414 414 FileNode('file_B', content='content_of_ancestor_child')]},
415 415 {'message': 'ancestor-child-2'},
416 416 ]
417 417 commit_ids = backend.create_master_repo(commits)
418 418 target = backend.create_repo(heads=['ancestor-child'])
419 419 source = backend.create_repo(heads=['change'])
420 420
421 421 response = self.app.post(
422 422 route_path('pullrequest_create', repo_name=source.repo_name),
423 423 [
424 424 ('source_repo', source.repo_name),
425 425 ('source_ref', 'branch:default:' + commit_ids['change']),
426 426 ('target_repo', target.repo_name),
427 427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
428 428 ('common_ancestor', commit_ids['ancestor']),
429 429 ('pullrequest_desc', 'Description'),
430 430 ('pullrequest_title', 'Title'),
431 431 ('__start__', 'review_members:sequence'),
432 432 ('__start__', 'reviewer:mapping'),
433 433 ('user_id', '2'),
434 434 ('__start__', 'reasons:sequence'),
435 435 ('reason', 'Some reason'),
436 436 ('__end__', 'reasons:sequence'),
437 437 ('mandatory', 'False'),
438 438 ('__end__', 'reviewer:mapping'),
439 439 ('__end__', 'review_members:sequence'),
440 440 ('__start__', 'revisions:sequence'),
441 441 ('revisions', commit_ids['change']),
442 442 ('__end__', 'revisions:sequence'),
443 443 ('user', ''),
444 444 ('csrf_token', csrf_token),
445 445 ],
446 446 status=302)
447 447
448 448 location = response.headers['Location']
449 449
450 450 pull_request_id = location.rsplit('/', 1)[1]
451 451 assert pull_request_id != 'new'
452 452 pull_request = PullRequest.get(int(pull_request_id))
453 453
454 454 # Check that a notification was made
455 455 notifications = Notification.query()\
456 456 .filter(Notification.created_by == pull_request.author.user_id,
457 457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
458 458 Notification.subject.contains(
459 459 "wants you to review pull request #%s" % pull_request_id))
460 460 assert len(notifications.all()) == 1
461 461
462 462 # Change reviewers and check that a notification was made
463 463 PullRequestModel().update_reviewers(
464 464 pull_request.pull_request_id, [(1, [], False)],
465 465 pull_request.author)
466 466 assert len(notifications.all()) == 2
467 467
468 468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
469 469 csrf_token):
470 470 commits = [
471 471 {'message': 'ancestor',
472 472 'added': [FileNode('file_A', content='content_of_ancestor')]},
473 473 {'message': 'change',
474 474 'added': [FileNode('file_a', content='content_of_change')]},
475 475 {'message': 'change-child'},
476 476 {'message': 'ancestor-child', 'parents': ['ancestor'],
477 477 'added': [
478 478 FileNode('file_B', content='content_of_ancestor_child')]},
479 479 {'message': 'ancestor-child-2'},
480 480 ]
481 481 commit_ids = backend.create_master_repo(commits)
482 482 target = backend.create_repo(heads=['ancestor-child'])
483 483 source = backend.create_repo(heads=['change'])
484 484
485 485 response = self.app.post(
486 486 route_path('pullrequest_create', repo_name=source.repo_name),
487 487 [
488 488 ('source_repo', source.repo_name),
489 489 ('source_ref', 'branch:default:' + commit_ids['change']),
490 490 ('target_repo', target.repo_name),
491 491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
492 492 ('common_ancestor', commit_ids['ancestor']),
493 493 ('pullrequest_desc', 'Description'),
494 494 ('pullrequest_title', 'Title'),
495 495 ('__start__', 'review_members:sequence'),
496 496 ('__start__', 'reviewer:mapping'),
497 497 ('user_id', '1'),
498 498 ('__start__', 'reasons:sequence'),
499 499 ('reason', 'Some reason'),
500 500 ('__end__', 'reasons:sequence'),
501 501 ('mandatory', 'False'),
502 502 ('__end__', 'reviewer:mapping'),
503 503 ('__end__', 'review_members:sequence'),
504 504 ('__start__', 'revisions:sequence'),
505 505 ('revisions', commit_ids['change']),
506 506 ('__end__', 'revisions:sequence'),
507 507 ('user', ''),
508 508 ('csrf_token', csrf_token),
509 509 ],
510 510 status=302)
511 511
512 512 location = response.headers['Location']
513 513
514 514 pull_request_id = location.rsplit('/', 1)[1]
515 515 assert pull_request_id != 'new'
516 516 pull_request = PullRequest.get(int(pull_request_id))
517 517
518 518 # target_ref has to point to the ancestor's commit_id in order to
519 519 # show the correct diff
520 520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
521 521 assert pull_request.target_ref == expected_target_ref
522 522
523 523 # Check generated diff contents
524 524 response = response.follow()
525 525 assert 'content_of_ancestor' not in response.body
526 526 assert 'content_of_ancestor-child' not in response.body
527 527 assert 'content_of_change' in response.body
528 528
529 529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
530 530 # Clear any previous calls to rcextensions
531 531 rhodecode.EXTENSIONS.calls.clear()
532 532
533 533 pull_request = pr_util.create_pull_request(
534 534 approved=True, mergeable=True)
535 535 pull_request_id = pull_request.pull_request_id
536 536 repo_name = pull_request.target_repo.scm_instance().name,
537 537
538 538 response = self.app.post(
539 539 route_path('pullrequest_merge',
540 540 repo_name=str(repo_name[0]),
541 541 pull_request_id=pull_request_id),
542 542 params={'csrf_token': csrf_token}).follow()
543 543
544 544 pull_request = PullRequest.get(pull_request_id)
545 545
546 546 assert response.status_int == 200
547 547 assert pull_request.is_closed()
548 548 assert_pull_request_status(
549 549 pull_request, ChangesetStatus.STATUS_APPROVED)
550 550
551 551 # Check the relevant log entries were added
552 552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
553 553 actions = [log.action for log in user_logs]
554 554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
555 555 expected_actions = [
556 556 u'repo.pull_request.close',
557 557 u'repo.pull_request.merge',
558 558 u'repo.pull_request.comment.create'
559 559 ]
560 560 assert actions == expected_actions
561 561
562 562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
563 563 actions = [log for log in user_logs]
564 564 assert actions[-1].action == 'user.push'
565 565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
566 566
567 567 # Check post_push rcextension was really executed
568 568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
569 569 assert len(push_calls) == 1
570 570 unused_last_call_args, last_call_kwargs = push_calls[0]
571 571 assert last_call_kwargs['action'] == 'push'
572 572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
573 573
574 574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
575 575 pull_request = pr_util.create_pull_request(mergeable=False)
576 576 pull_request_id = pull_request.pull_request_id
577 577 pull_request = PullRequest.get(pull_request_id)
578 578
579 579 response = self.app.post(
580 580 route_path('pullrequest_merge',
581 581 repo_name=pull_request.target_repo.scm_instance().name,
582 582 pull_request_id=pull_request.pull_request_id),
583 583 params={'csrf_token': csrf_token}).follow()
584 584
585 585 assert response.status_int == 200
586 586 response.mustcontain(
587 587 'Merge is not currently possible because of below failed checks.')
588 588 response.mustcontain('Server-side pull request merging is disabled.')
589 589
590 590 @pytest.mark.skip_backends('svn')
591 591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
592 592 pull_request = pr_util.create_pull_request(mergeable=True)
593 593 pull_request_id = pull_request.pull_request_id
594 594 repo_name = pull_request.target_repo.scm_instance().name
595 595
596 596 response = self.app.post(
597 597 route_path('pullrequest_merge',
598 598 repo_name=repo_name,
599 599 pull_request_id=pull_request_id),
600 600 params={'csrf_token': csrf_token}).follow()
601 601
602 602 assert response.status_int == 200
603 603
604 604 response.mustcontain(
605 605 'Merge is not currently possible because of below failed checks.')
606 606 response.mustcontain('Pull request reviewer approval is pending.')
607 607
608 608 def test_merge_pull_request_renders_failure_reason(
609 609 self, user_regular, csrf_token, pr_util):
610 610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 611 pull_request_id = pull_request.pull_request_id
612 612 repo_name = pull_request.target_repo.scm_instance().name
613 613
614 614 model_patcher = mock.patch.multiple(
615 615 PullRequestModel,
616 616 merge=mock.Mock(return_value=MergeResponse(
617 617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619 619
620 620 with model_patcher:
621 621 response = self.app.post(
622 622 route_path('pullrequest_merge',
623 623 repo_name=repo_name,
624 624 pull_request_id=pull_request_id),
625 625 params={'csrf_token': csrf_token}, status=302)
626 626
627 627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 628 MergeFailureReason.PUSH_FAILED])
629 629
630 630 def test_update_source_revision(self, backend, csrf_token):
631 631 commits = [
632 632 {'message': 'ancestor'},
633 633 {'message': 'change'},
634 634 {'message': 'change-2'},
635 635 ]
636 636 commit_ids = backend.create_master_repo(commits)
637 637 target = backend.create_repo(heads=['ancestor'])
638 638 source = backend.create_repo(heads=['change'])
639 639
640 640 # create pr from a in source to A in target
641 641 pull_request = PullRequest()
642 642 pull_request.source_repo = source
643 643 # TODO: johbo: Make sure that we write the source ref this way!
644 644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
645 645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
646 646 pull_request.target_repo = target
647 647
648 648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
649 649 branch=backend.default_branch_name,
650 650 commit_id=commit_ids['ancestor'])
651 651 pull_request.revisions = [commit_ids['change']]
652 652 pull_request.title = u"Test"
653 653 pull_request.description = u"Description"
654 654 pull_request.author = UserModel().get_by_username(
655 655 TEST_USER_ADMIN_LOGIN)
656 656 Session().add(pull_request)
657 657 Session().commit()
658 658 pull_request_id = pull_request.pull_request_id
659 659
660 660 # source has ancestor - change - change-2
661 661 backend.pull_heads(source, heads=['change-2'])
662 662
663 663 # update PR
664 664 self.app.post(
665 665 route_path('pullrequest_update',
666 666 repo_name=target.repo_name,
667 667 pull_request_id=pull_request_id),
668 668 params={'update_commits': 'true',
669 669 'csrf_token': csrf_token})
670 670
671 671 # check that we have now both revisions
672 672 pull_request = PullRequest.get(pull_request_id)
673 673 assert pull_request.revisions == [
674 674 commit_ids['change-2'], commit_ids['change']]
675 675
676 676 # TODO: johbo: this should be a test on its own
677 677 response = self.app.get(route_path(
678 678 'pullrequest_new',
679 679 repo_name=target.repo_name))
680 680 assert response.status_int == 200
681 681 assert 'Pull request updated to' in response.body
682 682 assert 'with 1 added, 0 removed commits.' in response.body
683 683
684 684 def test_update_target_revision(self, backend, csrf_token):
685 685 commits = [
686 686 {'message': 'ancestor'},
687 687 {'message': 'change'},
688 688 {'message': 'ancestor-new', 'parents': ['ancestor']},
689 689 {'message': 'change-rebased'},
690 690 ]
691 691 commit_ids = backend.create_master_repo(commits)
692 692 target = backend.create_repo(heads=['ancestor'])
693 693 source = backend.create_repo(heads=['change'])
694 694
695 695 # create pr from a in source to A in target
696 696 pull_request = PullRequest()
697 697 pull_request.source_repo = source
698 698 # TODO: johbo: Make sure that we write the source ref this way!
699 699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
700 700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
701 701 pull_request.target_repo = target
702 702 # TODO: johbo: Target ref should be branch based, since tip can jump
703 703 # from branch to branch
704 704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
705 705 branch=backend.default_branch_name,
706 706 commit_id=commit_ids['ancestor'])
707 707 pull_request.revisions = [commit_ids['change']]
708 708 pull_request.title = u"Test"
709 709 pull_request.description = u"Description"
710 710 pull_request.author = UserModel().get_by_username(
711 711 TEST_USER_ADMIN_LOGIN)
712 712 Session().add(pull_request)
713 713 Session().commit()
714 714 pull_request_id = pull_request.pull_request_id
715 715
716 716 # target has ancestor - ancestor-new
717 717 # source has ancestor - ancestor-new - change-rebased
718 718 backend.pull_heads(target, heads=['ancestor-new'])
719 719 backend.pull_heads(source, heads=['change-rebased'])
720 720
721 721 # update PR
722 722 self.app.post(
723 723 route_path('pullrequest_update',
724 724 repo_name=target.repo_name,
725 725 pull_request_id=pull_request_id),
726 726 params={'update_commits': 'true',
727 727 'csrf_token': csrf_token},
728 728 status=200)
729 729
730 730 # check that we have now both revisions
731 731 pull_request = PullRequest.get(pull_request_id)
732 732 assert pull_request.revisions == [commit_ids['change-rebased']]
733 733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
734 734 branch=backend.default_branch_name,
735 735 commit_id=commit_ids['ancestor-new'])
736 736
737 737 # TODO: johbo: This should be a test on its own
738 738 response = self.app.get(route_path(
739 739 'pullrequest_new',
740 740 repo_name=target.repo_name))
741 741 assert response.status_int == 200
742 742 assert 'Pull request updated to' in response.body
743 743 assert 'with 1 added, 1 removed commits.' in response.body
744 744
745 745 def test_update_of_ancestor_reference(self, backend, csrf_token):
746 746 commits = [
747 747 {'message': 'ancestor'},
748 748 {'message': 'change'},
749 749 {'message': 'change-2'},
750 750 {'message': 'ancestor-new', 'parents': ['ancestor']},
751 751 {'message': 'change-rebased'},
752 752 ]
753 753 commit_ids = backend.create_master_repo(commits)
754 754 target = backend.create_repo(heads=['ancestor'])
755 755 source = backend.create_repo(heads=['change'])
756 756
757 757 # create pr from a in source to A in target
758 758 pull_request = PullRequest()
759 759 pull_request.source_repo = source
760 760 # TODO: johbo: Make sure that we write the source ref this way!
761 761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
762 762 branch=backend.default_branch_name,
763 763 commit_id=commit_ids['change'])
764 764 pull_request.target_repo = target
765 765 # TODO: johbo: Target ref should be branch based, since tip can jump
766 766 # from branch to branch
767 767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
768 768 branch=backend.default_branch_name,
769 769 commit_id=commit_ids['ancestor'])
770 770 pull_request.revisions = [commit_ids['change']]
771 771 pull_request.title = u"Test"
772 772 pull_request.description = u"Description"
773 773 pull_request.author = UserModel().get_by_username(
774 774 TEST_USER_ADMIN_LOGIN)
775 775 Session().add(pull_request)
776 776 Session().commit()
777 777 pull_request_id = pull_request.pull_request_id
778 778
779 779 # target has ancestor - ancestor-new
780 780 # source has ancestor - ancestor-new - change-rebased
781 781 backend.pull_heads(target, heads=['ancestor-new'])
782 782 backend.pull_heads(source, heads=['change-rebased'])
783 783
784 784 # update PR
785 785 self.app.post(
786 786 route_path('pullrequest_update',
787 787 repo_name=target.repo_name,
788 788 pull_request_id=pull_request_id),
789 789 params={'update_commits': 'true',
790 790 'csrf_token': csrf_token},
791 791 status=200)
792 792
793 793 # Expect the target reference to be updated correctly
794 794 pull_request = PullRequest.get(pull_request_id)
795 795 assert pull_request.revisions == [commit_ids['change-rebased']]
796 796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
797 797 branch=backend.default_branch_name,
798 798 commit_id=commit_ids['ancestor-new'])
799 799 assert pull_request.target_ref == expected_target_ref
800 800
801 801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
802 802 branch_name = 'development'
803 803 commits = [
804 804 {'message': 'initial-commit'},
805 805 {'message': 'old-feature'},
806 806 {'message': 'new-feature', 'branch': branch_name},
807 807 ]
808 808 repo = backend_git.create_repo(commits)
809 809 commit_ids = backend_git.commit_ids
810 810
811 811 pull_request = PullRequest()
812 812 pull_request.source_repo = repo
813 813 pull_request.target_repo = repo
814 814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
815 815 branch=branch_name, commit_id=commit_ids['new-feature'])
816 816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
817 817 branch=backend_git.default_branch_name,
818 818 commit_id=commit_ids['old-feature'])
819 819 pull_request.revisions = [commit_ids['new-feature']]
820 820 pull_request.title = u"Test"
821 821 pull_request.description = u"Description"
822 822 pull_request.author = UserModel().get_by_username(
823 823 TEST_USER_ADMIN_LOGIN)
824 824 Session().add(pull_request)
825 825 Session().commit()
826 826
827 827 vcs = repo.scm_instance()
828 828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
829 829
830 830 response = self.app.get(route_path(
831 831 'pullrequest_show',
832 832 repo_name=repo.repo_name,
833 833 pull_request_id=pull_request.pull_request_id))
834 834
835 835 assert response.status_int == 200
836 836 assert_response = AssertResponse(response)
837 837 assert_response.element_contains(
838 838 '#changeset_compare_view_content .alert strong',
839 839 'Missing commits')
840 840 assert_response.element_contains(
841 841 '#changeset_compare_view_content .alert',
842 842 'This pull request cannot be displayed, because one or more'
843 843 ' commits no longer exist in the source repository.')
844 844
845 845 def test_strip_commits_from_pull_request(
846 846 self, backend, pr_util, csrf_token):
847 847 commits = [
848 848 {'message': 'initial-commit'},
849 849 {'message': 'old-feature'},
850 850 {'message': 'new-feature', 'parents': ['initial-commit']},
851 851 ]
852 852 pull_request = pr_util.create_pull_request(
853 853 commits, target_head='initial-commit', source_head='new-feature',
854 854 revisions=['new-feature'])
855 855
856 856 vcs = pr_util.source_repository.scm_instance()
857 857 if backend.alias == 'git':
858 858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
859 859 else:
860 860 vcs.strip(pr_util.commit_ids['new-feature'])
861 861
862 862 response = self.app.get(route_path(
863 863 'pullrequest_show',
864 864 repo_name=pr_util.target_repository.repo_name,
865 865 pull_request_id=pull_request.pull_request_id))
866 866
867 867 assert response.status_int == 200
868 868 assert_response = AssertResponse(response)
869 869 assert_response.element_contains(
870 870 '#changeset_compare_view_content .alert strong',
871 871 'Missing commits')
872 872 assert_response.element_contains(
873 873 '#changeset_compare_view_content .alert',
874 874 'This pull request cannot be displayed, because one or more'
875 875 ' commits no longer exist in the source repository.')
876 876 assert_response.element_contains(
877 877 '#update_commits',
878 878 'Update commits')
879 879
880 880 def test_strip_commits_and_update(
881 881 self, backend, pr_util, csrf_token):
882 882 commits = [
883 883 {'message': 'initial-commit'},
884 884 {'message': 'old-feature'},
885 885 {'message': 'new-feature', 'parents': ['old-feature']},
886 886 ]
887 887 pull_request = pr_util.create_pull_request(
888 888 commits, target_head='old-feature', source_head='new-feature',
889 889 revisions=['new-feature'], mergeable=True)
890 890
891 891 vcs = pr_util.source_repository.scm_instance()
892 892 if backend.alias == 'git':
893 893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
894 894 else:
895 895 vcs.strip(pr_util.commit_ids['new-feature'])
896 896
897 897 response = self.app.post(
898 898 route_path('pullrequest_update',
899 899 repo_name=pull_request.target_repo.repo_name,
900 900 pull_request_id=pull_request.pull_request_id),
901 901 params={'update_commits': 'true',
902 902 'csrf_token': csrf_token})
903 903
904 904 assert response.status_int == 200
905 905 assert response.body == 'true'
906 906
907 907 # Make sure that after update, it won't raise 500 errors
908 908 response = self.app.get(route_path(
909 909 'pullrequest_show',
910 910 repo_name=pr_util.target_repository.repo_name,
911 911 pull_request_id=pull_request.pull_request_id))
912 912
913 913 assert response.status_int == 200
914 914 assert_response = AssertResponse(response)
915 915 assert_response.element_contains(
916 916 '#changeset_compare_view_content .alert strong',
917 917 'Missing commits')
918 918
919 919 def test_branch_is_a_link(self, pr_util):
920 920 pull_request = pr_util.create_pull_request()
921 921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
922 922 pull_request.target_ref = 'branch:target:abcdef1234567890'
923 923 Session().add(pull_request)
924 924 Session().commit()
925 925
926 926 response = self.app.get(route_path(
927 927 'pullrequest_show',
928 928 repo_name=pull_request.target_repo.scm_instance().name,
929 929 pull_request_id=pull_request.pull_request_id))
930 930 assert response.status_int == 200
931 931 assert_response = AssertResponse(response)
932 932
933 933 origin = assert_response.get_element('.pr-origininfo .tag')
934 934 origin_children = origin.getchildren()
935 935 assert len(origin_children) == 1
936 936 target = assert_response.get_element('.pr-targetinfo .tag')
937 937 target_children = target.getchildren()
938 938 assert len(target_children) == 1
939 939
940 940 expected_origin_link = route_path(
941 941 'repo_changelog',
942 942 repo_name=pull_request.source_repo.scm_instance().name,
943 943 params=dict(branch='origin'))
944 944 expected_target_link = route_path(
945 945 'repo_changelog',
946 946 repo_name=pull_request.target_repo.scm_instance().name,
947 947 params=dict(branch='target'))
948 948 assert origin_children[0].attrib['href'] == expected_origin_link
949 949 assert origin_children[0].text == 'branch: origin'
950 950 assert target_children[0].attrib['href'] == expected_target_link
951 951 assert target_children[0].text == 'branch: target'
952 952
953 953 def test_bookmark_is_not_a_link(self, pr_util):
954 954 pull_request = pr_util.create_pull_request()
955 955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
956 956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
957 957 Session().add(pull_request)
958 958 Session().commit()
959 959
960 960 response = self.app.get(route_path(
961 961 'pullrequest_show',
962 962 repo_name=pull_request.target_repo.scm_instance().name,
963 963 pull_request_id=pull_request.pull_request_id))
964 964 assert response.status_int == 200
965 965 assert_response = AssertResponse(response)
966 966
967 967 origin = assert_response.get_element('.pr-origininfo .tag')
968 968 assert origin.text.strip() == 'bookmark: origin'
969 969 assert origin.getchildren() == []
970 970
971 971 target = assert_response.get_element('.pr-targetinfo .tag')
972 972 assert target.text.strip() == 'bookmark: target'
973 973 assert target.getchildren() == []
974 974
975 975 def test_tag_is_not_a_link(self, pr_util):
976 976 pull_request = pr_util.create_pull_request()
977 977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
978 978 pull_request.target_ref = 'tag:target:abcdef1234567890'
979 979 Session().add(pull_request)
980 980 Session().commit()
981 981
982 982 response = self.app.get(route_path(
983 983 'pullrequest_show',
984 984 repo_name=pull_request.target_repo.scm_instance().name,
985 985 pull_request_id=pull_request.pull_request_id))
986 986 assert response.status_int == 200
987 987 assert_response = AssertResponse(response)
988 988
989 989 origin = assert_response.get_element('.pr-origininfo .tag')
990 990 assert origin.text.strip() == 'tag: origin'
991 991 assert origin.getchildren() == []
992 992
993 993 target = assert_response.get_element('.pr-targetinfo .tag')
994 994 assert target.text.strip() == 'tag: target'
995 995 assert target.getchildren() == []
996 996
997 997 @pytest.mark.parametrize('mergeable', [True, False])
998 998 def test_shadow_repository_link(
999 999 self, mergeable, pr_util, http_host_only_stub):
1000 1000 """
1001 1001 Check that the pull request summary page displays a link to the shadow
1002 1002 repository if the pull request is mergeable. If it is not mergeable
1003 1003 the link should not be displayed.
1004 1004 """
1005 1005 pull_request = pr_util.create_pull_request(
1006 1006 mergeable=mergeable, enable_notifications=False)
1007 1007 target_repo = pull_request.target_repo.scm_instance()
1008 1008 pr_id = pull_request.pull_request_id
1009 1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1010 1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1011 1011
1012 1012 response = self.app.get(route_path(
1013 1013 'pullrequest_show',
1014 1014 repo_name=target_repo.name,
1015 1015 pull_request_id=pr_id))
1016 1016
1017 1017 assertr = AssertResponse(response)
1018 1018 if mergeable:
1019 1019 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1020 1020 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1021 1021 else:
1022 1022 assertr.no_element_exists('.pr-mergeinfo')
1023 1023
1024 1024
1025 1025 @pytest.mark.usefixtures('app')
1026 1026 @pytest.mark.backends("git", "hg")
1027 1027 class TestPullrequestsControllerDelete(object):
1028 1028 def test_pull_request_delete_button_permissions_admin(
1029 1029 self, autologin_user, user_admin, pr_util):
1030 1030 pull_request = pr_util.create_pull_request(
1031 1031 author=user_admin.username, enable_notifications=False)
1032 1032
1033 1033 response = self.app.get(route_path(
1034 1034 'pullrequest_show',
1035 1035 repo_name=pull_request.target_repo.scm_instance().name,
1036 1036 pull_request_id=pull_request.pull_request_id))
1037 1037
1038 1038 response.mustcontain('id="delete_pullrequest"')
1039 1039 response.mustcontain('Confirm to delete this pull request')
1040 1040
1041 1041 def test_pull_request_delete_button_permissions_owner(
1042 1042 self, autologin_regular_user, user_regular, pr_util):
1043 1043 pull_request = pr_util.create_pull_request(
1044 1044 author=user_regular.username, enable_notifications=False)
1045 1045
1046 1046 response = self.app.get(route_path(
1047 1047 'pullrequest_show',
1048 1048 repo_name=pull_request.target_repo.scm_instance().name,
1049 1049 pull_request_id=pull_request.pull_request_id))
1050 1050
1051 1051 response.mustcontain('id="delete_pullrequest"')
1052 1052 response.mustcontain('Confirm to delete this pull request')
1053 1053
1054 1054 def test_pull_request_delete_button_permissions_forbidden(
1055 1055 self, autologin_regular_user, user_regular, user_admin, pr_util):
1056 1056 pull_request = pr_util.create_pull_request(
1057 1057 author=user_admin.username, enable_notifications=False)
1058 1058
1059 1059 response = self.app.get(route_path(
1060 1060 'pullrequest_show',
1061 1061 repo_name=pull_request.target_repo.scm_instance().name,
1062 1062 pull_request_id=pull_request.pull_request_id))
1063 1063 response.mustcontain(no=['id="delete_pullrequest"'])
1064 1064 response.mustcontain(no=['Confirm to delete this pull request'])
1065 1065
1066 1066 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1067 1067 self, autologin_regular_user, user_regular, user_admin, pr_util,
1068 1068 user_util):
1069 1069
1070 1070 pull_request = pr_util.create_pull_request(
1071 1071 author=user_admin.username, enable_notifications=False)
1072 1072
1073 1073 user_util.grant_user_permission_to_repo(
1074 1074 pull_request.target_repo, user_regular,
1075 1075 'repository.write')
1076 1076
1077 1077 response = self.app.get(route_path(
1078 1078 'pullrequest_show',
1079 1079 repo_name=pull_request.target_repo.scm_instance().name,
1080 1080 pull_request_id=pull_request.pull_request_id))
1081 1081
1082 1082 response.mustcontain('id="open_edit_pullrequest"')
1083 1083 response.mustcontain('id="delete_pullrequest"')
1084 1084 response.mustcontain(no=['Confirm to delete this pull request'])
1085 1085
1086 1086 def test_delete_comment_returns_404_if_comment_does_not_exist(
1087 self, autologin_user, pr_util, user_admin):
1087 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1088 1088
1089 1089 pull_request = pr_util.create_pull_request(
1090 1090 author=user_admin.username, enable_notifications=False)
1091 1091
1092 self.app.get(route_path(
1092 self.app.post(
1093 route_path(
1093 1094 'pullrequest_comment_delete',
1094 1095 repo_name=pull_request.target_repo.scm_instance().name,
1095 1096 pull_request_id=pull_request.pull_request_id,
1096 comment_id=1024404), status=404)
1097 comment_id=1024404),
1098 extra_environ=xhr_header,
1099 params={'csrf_token': csrf_token},
1100 status=404
1101 )
1102
1103 def test_delete_comment(
1104 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1105
1106 pull_request = pr_util.create_pull_request(
1107 author=user_admin.username, enable_notifications=False)
1108 comment = pr_util.create_comment()
1109 comment_id = comment.comment_id
1110
1111 response = self.app.post(
1112 route_path(
1113 'pullrequest_comment_delete',
1114 repo_name=pull_request.target_repo.scm_instance().name,
1115 pull_request_id=pull_request.pull_request_id,
1116 comment_id=comment_id),
1117 extra_environ=xhr_header,
1118 params={'csrf_token': csrf_token},
1119 status=200
1120 )
1121 assert response.body == 'true'
1097 1122
1098 1123
1099 1124 def assert_pull_request_status(pull_request, expected_status):
1100 1125 status = ChangesetStatusModel().calculated_review_status(
1101 1126 pull_request=pull_request)
1102 1127 assert status == expected_status
1103 1128
1104 1129
1105 1130 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1106 1131 @pytest.mark.usefixtures("autologin_user")
1107 1132 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1108 1133 response = app.get(
1109 1134 route_path(route, repo_name=backend_svn.repo_name), status=404)
1110 1135
@@ -1,1182 +1,1181 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 peppercorn
26 26 from pyramid.httpexceptions import (
27 27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 46 ChangesetComment, ChangesetStatus, Repository)
47 47 from rhodecode.model.forms import PullRequestForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 50 from rhodecode.model.scm import ScmModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 56
57 57 def load_default_context(self):
58 58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 60 c.repo_info = self.db_repo
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
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 '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
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 pull_request_id = self.request.matchdict.get('pull_request_id')
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user)
367 367 c.pr_merge_errors = _merge_check.error_details
368 368 c.pr_merge_possible = not _merge_check.failed
369 369 c.pr_merge_message = _merge_check.merge_msg
370 370
371 371 c.pull_request_review_status = _merge_check.review_status
372 372 if merge_checks:
373 373 self.request.override_renderer = \
374 374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 375 return self._get_template_context(c)
376 376
377 377 comments_model = CommentsModel()
378 378
379 379 # reviewers and statuses
380 380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382 382
383 383 # GENERAL COMMENTS with versions #
384 384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 385 q = q.order_by(ChangesetComment.comment_id.asc())
386 386 general_comments = q
387 387
388 388 # pick comments we want to render at current version
389 389 c.comment_versions = comments_model.aggregate_comments(
390 390 general_comments, versions, c.at_version_num)
391 391 c.comments = c.comment_versions[c.at_version_num]['until']
392 392
393 393 # INLINE COMMENTS with versions #
394 394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 395 q = q.order_by(ChangesetComment.comment_id.asc())
396 396 inline_comments = q
397 397
398 398 c.inline_versions = comments_model.aggregate_comments(
399 399 inline_comments, versions, c.at_version_num, inline=True)
400 400
401 401 # inject latest version
402 402 latest_ver = PullRequest.get_pr_display_object(
403 403 pull_request_latest, pull_request_latest)
404 404
405 405 c.versions = versions + [latest_ver]
406 406
407 407 # if we use version, then do not show later comments
408 408 # than current version
409 409 display_inline_comments = collections.defaultdict(
410 410 lambda: collections.defaultdict(list))
411 411 for co in inline_comments:
412 412 if c.at_version_num:
413 413 # pick comments that are at least UPTO given version, so we
414 414 # don't render comments for higher version
415 415 should_render = co.pull_request_version_id and \
416 416 co.pull_request_version_id <= c.at_version_num
417 417 else:
418 418 # showing all, for 'latest'
419 419 should_render = True
420 420
421 421 if should_render:
422 422 display_inline_comments[co.f_path][co.line_no].append(co)
423 423
424 424 # load diff data into template context, if we use compare mode then
425 425 # diff is calculated based on changes between versions of PR
426 426
427 427 source_repo = pull_request_at_ver.source_repo
428 428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429 429
430 430 target_repo = pull_request_at_ver.target_repo
431 431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432 432
433 433 if compare:
434 434 # in compare switch the diff base to latest commit from prev version
435 435 target_ref_id = prev_pull_request_display_obj.revisions[0]
436 436
437 437 # despite opening commits for bookmarks/branches/tags, we always
438 438 # convert this to rev to prevent changes after bookmark or branch change
439 439 c.source_ref_type = 'rev'
440 440 c.source_ref = source_ref_id
441 441
442 442 c.target_ref_type = 'rev'
443 443 c.target_ref = target_ref_id
444 444
445 445 c.source_repo = source_repo
446 446 c.target_repo = target_repo
447 447
448 448 c.commit_ranges = []
449 449 source_commit = EmptyCommit()
450 450 target_commit = EmptyCommit()
451 451 c.missing_requirements = False
452 452
453 453 source_scm = source_repo.scm_instance()
454 454 target_scm = target_repo.scm_instance()
455 455
456 456 # try first shadow repo, fallback to regular repo
457 457 try:
458 458 commits_source_repo = pull_request_latest.get_shadow_repo()
459 459 except Exception:
460 460 log.debug('Failed to get shadow repo', exc_info=True)
461 461 commits_source_repo = source_scm
462 462
463 463 c.commits_source_repo = commits_source_repo
464 464 commit_cache = {}
465 465 try:
466 466 pre_load = ["author", "branch", "date", "message"]
467 467 show_revs = pull_request_at_ver.revisions
468 468 for rev in show_revs:
469 469 comm = commits_source_repo.get_commit(
470 470 commit_id=rev, pre_load=pre_load)
471 471 c.commit_ranges.append(comm)
472 472 commit_cache[comm.raw_id] = comm
473 473
474 474 # Order here matters, we first need to get target, and then
475 475 # the source
476 476 target_commit = commits_source_repo.get_commit(
477 477 commit_id=safe_str(target_ref_id))
478 478
479 479 source_commit = commits_source_repo.get_commit(
480 480 commit_id=safe_str(source_ref_id))
481 481
482 482 except CommitDoesNotExistError:
483 483 log.warning(
484 484 'Failed to get commit from `{}` repo'.format(
485 485 commits_source_repo), exc_info=True)
486 486 except RepositoryRequirementError:
487 487 log.warning(
488 488 'Failed to get all required data from repo', exc_info=True)
489 489 c.missing_requirements = True
490 490
491 491 c.ancestor = None # set it to None, to hide it from PR view
492 492
493 493 try:
494 494 ancestor_id = source_scm.get_common_ancestor(
495 495 source_commit.raw_id, target_commit.raw_id, target_scm)
496 496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 497 except Exception:
498 498 c.ancestor_commit = None
499 499
500 500 c.statuses = source_repo.statuses(
501 501 [x.raw_id for x in c.commit_ranges])
502 502
503 503 # auto collapse if we have more than limit
504 504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 506 c.compare_mode = compare
507 507
508 508 # diff_limit is the old behavior, will cut off the whole diff
509 509 # if the limit is applied otherwise will just hide the
510 510 # big files from the front-end
511 511 diff_limit = c.visual.cut_off_limit_diff
512 512 file_limit = c.visual.cut_off_limit_file
513 513
514 514 c.missing_commits = False
515 515 if (c.missing_requirements
516 516 or isinstance(source_commit, EmptyCommit)
517 517 or source_commit == target_commit):
518 518
519 519 c.missing_commits = True
520 520 else:
521 521
522 522 c.diffset = self._get_diffset(
523 523 c.source_repo.repo_name, commits_source_repo,
524 524 source_ref_id, target_ref_id,
525 525 target_commit, source_commit,
526 526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527 527
528 528 c.limited_diff = c.diffset.limited_diff
529 529
530 530 # calculate removed files that are bound to comments
531 531 comment_deleted_files = [
532 532 fname for fname in display_inline_comments
533 533 if fname not in c.diffset.file_stats]
534 534
535 535 c.deleted_files_comments = collections.defaultdict(dict)
536 536 for fname, per_line_comments in display_inline_comments.items():
537 537 if fname in comment_deleted_files:
538 538 c.deleted_files_comments[fname]['stats'] = 0
539 539 c.deleted_files_comments[fname]['comments'] = list()
540 540 for lno, comments in per_line_comments.items():
541 541 c.deleted_files_comments[fname]['comments'].extend(
542 542 comments)
543 543
544 544 # this is a hack to properly display links, when creating PR, the
545 545 # compare view and others uses different notation, and
546 546 # compare_commits.mako renders links based on the target_repo.
547 547 # We need to swap that here to generate it properly on the html side
548 548 c.target_repo = c.source_repo
549 549
550 550 c.commit_statuses = ChangesetStatus.STATUSES
551 551
552 552 c.show_version_changes = not pr_closed
553 553 if c.show_version_changes:
554 554 cur_obj = pull_request_at_ver
555 555 prev_obj = prev_pull_request_at_ver
556 556
557 557 old_commit_ids = prev_obj.revisions
558 558 new_commit_ids = cur_obj.revisions
559 559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 560 old_commit_ids, new_commit_ids)
561 561 c.commit_changes_summary = commit_changes
562 562
563 563 # calculate the diff for commits between versions
564 564 c.commit_changes = []
565 565 mark = lambda cs, fw: list(
566 566 h.itertools.izip_longest([], cs, fillvalue=fw))
567 567 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 568 + mark(commit_changes.removed, 'r') \
569 569 + mark(commit_changes.common, 'c'):
570 570
571 571 if raw_id in commit_cache:
572 572 commit = commit_cache[raw_id]
573 573 else:
574 574 try:
575 575 commit = commits_source_repo.get_commit(raw_id)
576 576 except CommitDoesNotExistError:
577 577 # in case we fail extracting still use "dummy" commit
578 578 # for display in commit diff
579 579 commit = h.AttributeDict(
580 580 {'raw_id': raw_id,
581 581 'message': 'EMPTY or MISSING COMMIT'})
582 582 c.commit_changes.append([c_type, commit])
583 583
584 584 # current user review statuses for each version
585 585 c.review_versions = {}
586 586 if self._rhodecode_user.user_id in allowed_reviewers:
587 587 for co in general_comments:
588 588 if co.author.user_id == self._rhodecode_user.user_id:
589 589 # each comment has a status change
590 590 status = co.status_change
591 591 if status:
592 592 _ver_pr = status[0].comment.pull_request_version_id
593 593 c.review_versions[_ver_pr] = status[0]
594 594
595 595 return self._get_template_context(c)
596 596
597 597 def assure_not_empty_repo(self):
598 598 _ = self.request.translate
599 599
600 600 try:
601 601 self.db_repo.scm_instance().get_commit()
602 602 except EmptyRepositoryError:
603 603 h.flash(h.literal(_('There are no commits yet')),
604 604 category='warning')
605 605 raise HTTPFound(
606 606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607 607
608 608 @LoginRequired()
609 609 @NotAnonymous()
610 610 @HasRepoPermissionAnyDecorator(
611 611 'repository.read', 'repository.write', 'repository.admin')
612 612 @view_config(
613 613 route_name='pullrequest_new', request_method='GET',
614 614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 615 def pull_request_new(self):
616 616 _ = self.request.translate
617 617 c = self.load_default_context()
618 618
619 619 self.assure_not_empty_repo()
620 620 source_repo = self.db_repo
621 621
622 622 commit_id = self.request.GET.get('commit')
623 623 branch_ref = self.request.GET.get('branch')
624 624 bookmark_ref = self.request.GET.get('bookmark')
625 625
626 626 try:
627 627 source_repo_data = PullRequestModel().generate_repo_data(
628 628 source_repo, commit_id=commit_id,
629 629 branch=branch_ref, bookmark=bookmark_ref)
630 630 except CommitDoesNotExistError as e:
631 631 log.exception(e)
632 632 h.flash(_('Commit does not exist'), 'error')
633 633 raise HTTPFound(
634 634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635 635
636 636 default_target_repo = source_repo
637 637
638 638 if source_repo.parent:
639 639 parent_vcs_obj = source_repo.parent.scm_instance()
640 640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 641 # change default if we have a parent repo
642 642 default_target_repo = source_repo.parent
643 643
644 644 target_repo_data = PullRequestModel().generate_repo_data(
645 645 default_target_repo)
646 646
647 647 selected_source_ref = source_repo_data['refs']['selected_ref']
648 648
649 649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 651 source=source_repo.repo_name,
652 652 source_ref=title_source_ref,
653 653 target=default_target_repo.repo_name
654 654 )
655 655
656 656 c.default_repo_data = {
657 657 'source_repo_name': source_repo.repo_name,
658 658 'source_refs_json': json.dumps(source_repo_data),
659 659 'target_repo_name': default_target_repo.repo_name,
660 660 'target_refs_json': json.dumps(target_repo_data),
661 661 }
662 662 c.default_source_ref = selected_source_ref
663 663
664 664 return self._get_template_context(c)
665 665
666 666 @LoginRequired()
667 667 @NotAnonymous()
668 668 @HasRepoPermissionAnyDecorator(
669 669 'repository.read', 'repository.write', 'repository.admin')
670 670 @view_config(
671 671 route_name='pullrequest_repo_refs', request_method='GET',
672 672 renderer='json_ext', xhr=True)
673 673 def pull_request_repo_refs(self):
674 674 target_repo_name = self.request.matchdict['target_repo_name']
675 675 repo = Repository.get_by_repo_name(target_repo_name)
676 676 if not repo:
677 677 raise HTTPNotFound()
678 678 return PullRequestModel().generate_repo_data(repo)
679 679
680 680 @LoginRequired()
681 681 @NotAnonymous()
682 682 @HasRepoPermissionAnyDecorator(
683 683 'repository.read', 'repository.write', 'repository.admin')
684 684 @view_config(
685 685 route_name='pullrequest_repo_destinations', request_method='GET',
686 686 renderer='json_ext', xhr=True)
687 687 def pull_request_repo_destinations(self):
688 688 _ = self.request.translate
689 689 filter_query = self.request.GET.get('query')
690 690
691 691 query = Repository.query() \
692 692 .order_by(func.length(Repository.repo_name)) \
693 693 .filter(
694 694 or_(Repository.repo_name == self.db_repo.repo_name,
695 695 Repository.fork_id == self.db_repo.repo_id))
696 696
697 697 if filter_query:
698 698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 699 query = query.filter(
700 700 Repository.repo_name.ilike(ilike_expression))
701 701
702 702 add_parent = False
703 703 if self.db_repo.parent:
704 704 if filter_query in self.db_repo.parent.repo_name:
705 705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 707 add_parent = True
708 708
709 709 limit = 20 - 1 if add_parent else 20
710 710 all_repos = query.limit(limit).all()
711 711 if add_parent:
712 712 all_repos += [self.db_repo.parent]
713 713
714 714 repos = []
715 715 for obj in ScmModel().get_repos(all_repos):
716 716 repos.append({
717 717 'id': obj['name'],
718 718 'text': obj['name'],
719 719 'type': 'repo',
720 720 'obj': obj['dbrepo']
721 721 })
722 722
723 723 data = {
724 724 'more': False,
725 725 'results': [{
726 726 'text': _('Repositories'),
727 727 'children': repos
728 728 }] if repos else []
729 729 }
730 730 return data
731 731
732 732 @LoginRequired()
733 733 @NotAnonymous()
734 734 @HasRepoPermissionAnyDecorator(
735 735 'repository.read', 'repository.write', 'repository.admin')
736 736 @CSRFRequired()
737 737 @view_config(
738 738 route_name='pullrequest_create', request_method='POST',
739 739 renderer=None)
740 740 def pull_request_create(self):
741 741 _ = self.request.translate
742 742 self.assure_not_empty_repo()
743 743
744 744 controls = peppercorn.parse(self.request.POST.items())
745 745
746 746 try:
747 747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 748 except formencode.Invalid as errors:
749 749 if errors.error_dict.get('revisions'):
750 750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 751 elif errors.error_dict.get('pullrequest_title'):
752 752 msg = _('Pull request requires a title with min. 3 chars')
753 753 else:
754 754 msg = _('Error creating pull request: {}').format(errors)
755 755 log.exception(msg)
756 756 h.flash(msg, 'error')
757 757
758 758 # would rather just go back to form ...
759 759 raise HTTPFound(
760 760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761 761
762 762 source_repo = _form['source_repo']
763 763 source_ref = _form['source_ref']
764 764 target_repo = _form['target_repo']
765 765 target_ref = _form['target_ref']
766 766 commit_ids = _form['revisions'][::-1]
767 767
768 768 # find the ancestor for this pr
769 769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771 771
772 772 source_scm = source_db_repo.scm_instance()
773 773 target_scm = target_db_repo.scm_instance()
774 774
775 775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777 777
778 778 ancestor = source_scm.get_common_ancestor(
779 779 source_commit.raw_id, target_commit.raw_id, target_scm)
780 780
781 781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783 783
784 784 pullrequest_title = _form['pullrequest_title']
785 785 title_source_ref = source_ref.split(':', 2)[1]
786 786 if not pullrequest_title:
787 787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 788 source=source_repo,
789 789 source_ref=title_source_ref,
790 790 target=target_repo
791 791 )
792 792
793 793 description = _form['pullrequest_desc']
794 794
795 795 get_default_reviewers_data, validate_default_reviewers = \
796 796 PullRequestModel().get_reviewer_functions()
797 797
798 798 # recalculate reviewers logic, to make sure we can validate this
799 799 reviewer_rules = get_default_reviewers_data(
800 800 self._rhodecode_db_user, source_db_repo,
801 801 source_commit, target_db_repo, target_commit)
802 802
803 803 given_reviewers = _form['review_members']
804 804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805 805
806 806 try:
807 807 pull_request = PullRequestModel().create(
808 808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 809 target_ref, commit_ids, reviewers, pullrequest_title,
810 810 description, reviewer_rules
811 811 )
812 812 Session().commit()
813 813 h.flash(_('Successfully opened new pull request'),
814 814 category='success')
815 815 except Exception as e:
816 816 msg = _('Error occurred during creation of this pull request.')
817 817 log.exception(msg)
818 818 h.flash(msg, category='error')
819 819 raise HTTPFound(
820 820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821 821
822 822 raise HTTPFound(
823 823 h.route_path('pullrequest_show', repo_name=target_repo,
824 824 pull_request_id=pull_request.pull_request_id))
825 825
826 826 @LoginRequired()
827 827 @NotAnonymous()
828 828 @HasRepoPermissionAnyDecorator(
829 829 'repository.read', 'repository.write', 'repository.admin')
830 830 @CSRFRequired()
831 831 @view_config(
832 832 route_name='pullrequest_update', request_method='POST',
833 833 renderer='json_ext')
834 834 def pull_request_update(self):
835 835 pull_request_id = self.request.matchdict['pull_request_id']
836 836 pull_request = PullRequest.get_or_404(pull_request_id)
837 837
838 838 # only owner or admin can update it
839 839 allowed_to_update = PullRequestModel().check_user_update(
840 840 pull_request, self._rhodecode_user)
841 841 if allowed_to_update:
842 842 controls = peppercorn.parse(self.request.POST.items())
843 843
844 844 if 'review_members' in controls:
845 845 self._update_reviewers(
846 846 pull_request_id, controls['review_members'],
847 847 pull_request.reviewer_data)
848 848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 849 self._update_commits(pull_request)
850 850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 851 self._edit_pull_request(pull_request)
852 852 else:
853 853 raise HTTPBadRequest()
854 854 return True
855 855 raise HTTPForbidden()
856 856
857 857 def _edit_pull_request(self, pull_request):
858 858 _ = self.request.translate
859 859 try:
860 860 PullRequestModel().edit(
861 861 pull_request, self.request.POST.get('title'),
862 862 self.request.POST.get('description'), self._rhodecode_user)
863 863 except ValueError:
864 864 msg = _(u'Cannot update closed pull requests.')
865 865 h.flash(msg, category='error')
866 866 return
867 867 else:
868 868 Session().commit()
869 869
870 870 msg = _(u'Pull request title & description updated.')
871 871 h.flash(msg, category='success')
872 872 return
873 873
874 874 def _update_commits(self, pull_request):
875 875 _ = self.request.translate
876 876 resp = PullRequestModel().update_commits(pull_request)
877 877
878 878 if resp.executed:
879 879
880 880 if resp.target_changed and resp.source_changed:
881 881 changed = 'target and source repositories'
882 882 elif resp.target_changed and not resp.source_changed:
883 883 changed = 'target repository'
884 884 elif not resp.target_changed and resp.source_changed:
885 885 changed = 'source repository'
886 886 else:
887 887 changed = 'nothing'
888 888
889 889 msg = _(
890 890 u'Pull request updated to "{source_commit_id}" with '
891 891 u'{count_added} added, {count_removed} removed commits. '
892 892 u'Source of changes: {change_source}')
893 893 msg = msg.format(
894 894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 895 count_added=len(resp.changes.added),
896 896 count_removed=len(resp.changes.removed),
897 897 change_source=changed)
898 898 h.flash(msg, category='success')
899 899
900 900 channel = '/repo${}$/pr/{}'.format(
901 901 pull_request.target_repo.repo_name,
902 902 pull_request.pull_request_id)
903 903 message = msg + (
904 904 ' - <a onclick="window.location.reload()">'
905 905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 906 channelstream.post_message(
907 907 channel, message, self._rhodecode_user.username,
908 908 registry=self.request.registry)
909 909 else:
910 910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 911 warning_reasons = [
912 912 UpdateFailureReason.NO_CHANGE,
913 913 UpdateFailureReason.WRONG_REF_TYPE,
914 914 ]
915 915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 916 h.flash(msg, category=category)
917 917
918 918 @LoginRequired()
919 919 @NotAnonymous()
920 920 @HasRepoPermissionAnyDecorator(
921 921 'repository.read', 'repository.write', 'repository.admin')
922 922 @CSRFRequired()
923 923 @view_config(
924 924 route_name='pullrequest_merge', request_method='POST',
925 925 renderer='json_ext')
926 926 def pull_request_merge(self):
927 927 """
928 928 Merge will perform a server-side merge of the specified
929 929 pull request, if the pull request is approved and mergeable.
930 930 After successful merging, the pull request is automatically
931 931 closed, with a relevant comment.
932 932 """
933 933 pull_request_id = self.request.matchdict['pull_request_id']
934 934 pull_request = PullRequest.get_or_404(pull_request_id)
935 935
936 936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 937 merge_possible = not check.failed
938 938
939 939 for err_type, error_msg in check.errors:
940 940 h.flash(error_msg, category=err_type)
941 941
942 942 if merge_possible:
943 943 log.debug("Pre-conditions checked, trying to merge.")
944 944 extras = vcs_operation_context(
945 945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 946 username=self._rhodecode_db_user.username, action='push',
947 947 scm=pull_request.target_repo.repo_type)
948 948 self._merge_pull_request(
949 949 pull_request, self._rhodecode_db_user, extras)
950 950 else:
951 951 log.debug("Pre-conditions failed, NOT merging.")
952 952
953 953 raise HTTPFound(
954 954 h.route_path('pullrequest_show',
955 955 repo_name=pull_request.target_repo.repo_name,
956 956 pull_request_id=pull_request.pull_request_id))
957 957
958 958 def _merge_pull_request(self, pull_request, user, extras):
959 959 _ = self.request.translate
960 960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961 961
962 962 if merge_resp.executed:
963 963 log.debug("The merge was successful, closing the pull request.")
964 964 PullRequestModel().close_pull_request(
965 965 pull_request.pull_request_id, user)
966 966 Session().commit()
967 967 msg = _('Pull request was successfully merged and closed.')
968 968 h.flash(msg, category='success')
969 969 else:
970 970 log.debug(
971 971 "The merge was not successful. Merge response: %s",
972 972 merge_resp)
973 973 msg = PullRequestModel().merge_status_message(
974 974 merge_resp.failure_reason)
975 975 h.flash(msg, category='error')
976 976
977 977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
978 978 _ = self.request.translate
979 979 get_default_reviewers_data, validate_default_reviewers = \
980 980 PullRequestModel().get_reviewer_functions()
981 981
982 982 try:
983 983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 984 except ValueError as e:
985 985 log.error('Reviewers Validation: {}'.format(e))
986 986 h.flash(e, category='error')
987 987 return
988 988
989 989 PullRequestModel().update_reviewers(
990 990 pull_request_id, reviewers, self._rhodecode_user)
991 991 h.flash(_('Pull request reviewers updated.'), category='success')
992 992 Session().commit()
993 993
994 994 @LoginRequired()
995 995 @NotAnonymous()
996 996 @HasRepoPermissionAnyDecorator(
997 997 'repository.read', 'repository.write', 'repository.admin')
998 998 @CSRFRequired()
999 999 @view_config(
1000 1000 route_name='pullrequest_delete', request_method='POST',
1001 1001 renderer='json_ext')
1002 1002 def pull_request_delete(self):
1003 1003 _ = self.request.translate
1004 1004
1005 1005 pull_request_id = self.request.matchdict['pull_request_id']
1006 1006 pull_request = PullRequest.get_or_404(pull_request_id)
1007 1007
1008 1008 pr_closed = pull_request.is_closed()
1009 1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 1010 pull_request, self._rhodecode_user) and not pr_closed
1011 1011
1012 1012 # only owner can delete it !
1013 1013 if allowed_to_delete:
1014 1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 1015 Session().commit()
1016 1016 h.flash(_('Successfully deleted pull request'),
1017 1017 category='success')
1018 1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019 1019
1020 1020 log.warning('user %s tried to delete pull request without access',
1021 1021 self._rhodecode_user)
1022 1022 raise HTTPNotFound()
1023 1023
1024 1024 @LoginRequired()
1025 1025 @NotAnonymous()
1026 1026 @HasRepoPermissionAnyDecorator(
1027 1027 'repository.read', 'repository.write', 'repository.admin')
1028 1028 @CSRFRequired()
1029 1029 @view_config(
1030 1030 route_name='pullrequest_comment_create', request_method='POST',
1031 1031 renderer='json_ext')
1032 1032 def pull_request_comment_create(self):
1033 1033 _ = self.request.translate
1034 1034 pull_request_id = self.request.matchdict['pull_request_id']
1035 1035 pull_request = PullRequest.get_or_404(pull_request_id)
1036 1036 if pull_request.is_closed():
1037 1037 log.debug('comment: forbidden because pull request is closed')
1038 1038 raise HTTPForbidden()
1039 1039
1040 1040 c = self.load_default_context()
1041 1041
1042 1042 status = self.request.POST.get('changeset_status', None)
1043 1043 text = self.request.POST.get('text')
1044 1044 comment_type = self.request.POST.get('comment_type')
1045 1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 1046 close_pull_request = self.request.POST.get('close_pull_request')
1047 1047
1048 1048 # the logic here should work like following, if we submit close
1049 1049 # pr comment, use `close_pull_request_with_comment` function
1050 1050 # else handle regular comment logic
1051 1051
1052 1052 if close_pull_request:
1053 1053 # only owner or admin or person with write permissions
1054 1054 allowed_to_close = PullRequestModel().check_user_update(
1055 1055 pull_request, self._rhodecode_user)
1056 1056 if not allowed_to_close:
1057 1057 log.debug('comment: forbidden because not allowed to close '
1058 1058 'pull request %s', pull_request_id)
1059 1059 raise HTTPForbidden()
1060 1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 1062 Session().flush()
1063 1063 events.trigger(
1064 1064 events.PullRequestCommentEvent(pull_request, comment))
1065 1065
1066 1066 else:
1067 1067 # regular comment case, could be inline, or one with status.
1068 1068 # for that one we check also permissions
1069 1069
1070 1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 1071 pull_request, self._rhodecode_user)
1072 1072
1073 1073 if status and allowed_to_change_status:
1074 1074 message = (_('Status change %(transition_icon)s %(status)s')
1075 1075 % {'transition_icon': '>',
1076 1076 'status': ChangesetStatus.get_status_lbl(status)})
1077 1077 text = text or message
1078 1078
1079 1079 comment = CommentsModel().create(
1080 1080 text=text,
1081 1081 repo=self.db_repo.repo_id,
1082 1082 user=self._rhodecode_user.user_id,
1083 1083 pull_request=pull_request_id,
1084 1084 f_path=self.request.POST.get('f_path'),
1085 1085 line_no=self.request.POST.get('line'),
1086 1086 status_change=(ChangesetStatus.get_status_lbl(status)
1087 1087 if status and allowed_to_change_status else None),
1088 1088 status_change_type=(status
1089 1089 if status and allowed_to_change_status else None),
1090 1090 comment_type=comment_type,
1091 1091 resolves_comment_id=resolves_comment_id
1092 1092 )
1093 1093
1094 1094 if allowed_to_change_status:
1095 1095 # calculate old status before we change it
1096 1096 old_calculated_status = pull_request.calculated_review_status()
1097 1097
1098 1098 # get status if set !
1099 1099 if status:
1100 1100 ChangesetStatusModel().set_status(
1101 1101 self.db_repo.repo_id,
1102 1102 status,
1103 1103 self._rhodecode_user.user_id,
1104 1104 comment,
1105 1105 pull_request=pull_request_id
1106 1106 )
1107 1107
1108 1108 Session().flush()
1109 1109 events.trigger(
1110 1110 events.PullRequestCommentEvent(pull_request, comment))
1111 1111
1112 1112 # we now calculate the status of pull request, and based on that
1113 1113 # calculation we set the commits status
1114 1114 calculated_status = pull_request.calculated_review_status()
1115 1115 if old_calculated_status != calculated_status:
1116 1116 PullRequestModel()._trigger_pull_request_hook(
1117 1117 pull_request, self._rhodecode_user, 'review_status_change')
1118 1118
1119 1119 Session().commit()
1120 1120
1121 1121 data = {
1122 1122 'target_id': h.safeid(h.safe_unicode(
1123 1123 self.request.POST.get('f_path'))),
1124 1124 }
1125 1125 if comment:
1126 1126 c.co = comment
1127 1127 rendered_comment = render(
1128 1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 1129 self._get_template_context(c), self.request)
1130 1130
1131 1131 data.update(comment.get_dict())
1132 1132 data.update({'rendered_text': rendered_comment})
1133 1133
1134 1134 return data
1135 1135
1136 1136 @LoginRequired()
1137 1137 @NotAnonymous()
1138 1138 @HasRepoPermissionAnyDecorator(
1139 1139 'repository.read', 'repository.write', 'repository.admin')
1140 1140 @CSRFRequired()
1141 1141 @view_config(
1142 1142 route_name='pullrequest_comment_delete', request_method='POST',
1143 1143 renderer='json_ext')
1144 1144 def pull_request_comment_delete(self):
1145 commit_id = self.request.matchdict['commit_id']
1145 pull_request_id = self.request.matchdict['pull_request_id']
1146 1146 comment_id = self.request.matchdict['comment_id']
1147 pull_request_id = self.request.matchdict['pull_request_id']
1148 1147
1149 1148 pull_request = PullRequest.get_or_404(pull_request_id)
1150 1149 if pull_request.is_closed():
1151 1150 log.debug('comment: forbidden because pull request is closed')
1152 1151 raise HTTPForbidden()
1153 1152
1154 1153 comment = ChangesetComment.get_or_404(comment_id)
1155 1154 if not comment:
1156 1155 log.debug('Comment with id:%s not found, skipping', comment_id)
1157 1156 # comment already deleted in another call probably
1158 1157 return True
1159 1158
1160 1159 if comment.pull_request.is_closed():
1161 1160 # don't allow deleting comments on closed pull request
1162 1161 raise HTTPForbidden()
1163 1162
1164 1163 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1165 1164 super_admin = h.HasPermissionAny('hg.admin')()
1166 1165 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1167 1166 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1168 1167 comment_repo_admin = is_repo_admin and is_repo_comment
1169 1168
1170 1169 if super_admin or comment_owner or comment_repo_admin:
1171 1170 old_calculated_status = comment.pull_request.calculated_review_status()
1172 1171 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1173 1172 Session().commit()
1174 1173 calculated_status = comment.pull_request.calculated_review_status()
1175 1174 if old_calculated_status != calculated_status:
1176 1175 PullRequestModel()._trigger_pull_request_hook(
1177 1176 comment.pull_request, self._rhodecode_user, 'review_status_change')
1178 1177 return True
1179 1178 else:
1180 1179 log.warning('No permissions for user %s to delete comment_id: %s',
1181 1180 self._rhodecode_db_user, comment_id)
1182 1181 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now