##// END OF EJS Templates
pull-requests: use proper diff calculation for versioning of PRs.
marcink -
r4426:f4c1c6eb default
parent child Browse files
Show More
@@ -1,1435 +1,1652 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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, Repository)
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.model.comment import CommentsModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
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 'repo_commits': '/{repo_name}/commits',
45 45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 59 }[name].format(**kwargs)
60 60
61 61 if params:
62 62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 63 return base_url
64 64
65 65
66 66 @pytest.mark.usefixtures('app', 'autologin_user')
67 67 @pytest.mark.backends("git", "hg")
68 68 class TestPullrequestsView(object):
69 69
70 70 def test_index(self, backend):
71 71 self.app.get(route_path(
72 72 'pullrequest_new',
73 73 repo_name=backend.repo_name))
74 74
75 75 def test_option_menu_create_pull_request_exists(self, backend):
76 76 repo_name = backend.repo_name
77 77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78 78
79 79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 80 'pullrequest_new', repo_name=repo_name)
81 81 response.mustcontain(create_pr_link)
82 82
83 83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 84 repo = backend.repo
85 85
86 86 self.app.get(
87 87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 88 commit=repo.get_commit().raw_id),
89 89 status=200)
90 90
91 91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 94 pull_request = pr_util.create_pull_request(
95 95 mergeable=pr_merge_enabled, enable_notifications=False)
96 96
97 97 response = self.app.get(route_path(
98 98 'pullrequest_show',
99 99 repo_name=pull_request.target_repo.scm_instance().name,
100 100 pull_request_id=pull_request.pull_request_id,
101 101 params={'range-diff': range_diff}))
102 102
103 103 for commit_id in pull_request.revisions:
104 104 response.mustcontain(commit_id)
105 105
106 106 response.mustcontain(pull_request.target_ref_parts.type)
107 107 response.mustcontain(pull_request.target_ref_parts.name)
108 108
109 109 response.mustcontain('class="pull-request-merge"')
110 110
111 111 if pr_merge_enabled:
112 112 response.mustcontain('Pull request reviewer approval is pending')
113 113 else:
114 114 response.mustcontain('Server-side pull request merging is disabled.')
115 115
116 116 if range_diff == "1":
117 117 response.mustcontain('Turn off: Show the diff as commit range')
118 118
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
127
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
138
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
153 ('source_repo', source.repo_name),
154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
176 ('csrf_token', csrf_token),
177 ],
178 status=302)
179
180 location = response.headers['Location']
181
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
185
186 pull_request_id = pull_request.pull_request_id
187
188 # Show initial version of PR
189 response = self.app.get(
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
193
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
203
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
228 # Show final version !
229 response = self.app.get(
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
233
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
253
254 # show v1,v2,v3,v4
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
261
262 commit_tests = {
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
275 for idx, ver in enumerate(versions, 1):
276
277 response = self.app.get(
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
286 yes, no = commit_tests[idx]
287 for y in yes:
288 response.mustcontain(y)
289 for n in no:
290 response.mustcontain(no=n)
291
292 yes, no = diff_tests[idx]
293 for y in yes:
294 response.mustcontain(cb_line(y))
295 for n in no:
296 response.mustcontain(no=n)
297
298 # show diff between versions
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
306
307 to_ver = ver+1
308 if idx == 3:
309 to_ver = 'latest'
310
311 response = self.app.get(
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
319 for a in adds:
320 response.mustcontain(cb_line(a))
321 for c in context:
322 response.mustcontain(cb_context(c))
323
324 # test version v2 -> v3
325 response = self.app.get(
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
330
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
335
119 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
120 337 # Logout
121 338 response = self.app.post(
122 339 h.route_path('logout'),
123 340 params={'csrf_token': csrf_token})
124 341 # Login as regular user
125 342 response = self.app.post(h.route_path('login'),
126 343 {'username': TEST_USER_REGULAR_LOGIN,
127 344 'password': 'test12'})
128 345
129 346 pull_request = pr_util.create_pull_request(
130 347 author=TEST_USER_REGULAR_LOGIN)
131 348
132 349 response = self.app.get(route_path(
133 350 'pullrequest_show',
134 351 repo_name=pull_request.target_repo.scm_instance().name,
135 352 pull_request_id=pull_request.pull_request_id))
136 353
137 354 response.mustcontain('Server-side pull request merging is disabled.')
138 355
139 356 assert_response = response.assert_response()
140 357 # for regular user without a merge permissions, we don't see it
141 358 assert_response.no_element_exists('#close-pull-request-action')
142 359
143 360 user_util.grant_user_permission_to_repo(
144 361 pull_request.target_repo,
145 362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
146 363 'repository.write')
147 364 response = self.app.get(route_path(
148 365 'pullrequest_show',
149 366 repo_name=pull_request.target_repo.scm_instance().name,
150 367 pull_request_id=pull_request.pull_request_id))
151 368
152 369 response.mustcontain('Server-side pull request merging is disabled.')
153 370
154 371 assert_response = response.assert_response()
155 372 # now regular user has a merge permissions, we have CLOSE button
156 373 assert_response.one_element_exists('#close-pull-request-action')
157 374
158 375 def test_show_invalid_commit_id(self, pr_util):
159 376 # Simulating invalid revisions which will cause a lookup error
160 377 pull_request = pr_util.create_pull_request()
161 378 pull_request.revisions = ['invalid']
162 379 Session().add(pull_request)
163 380 Session().commit()
164 381
165 382 response = self.app.get(route_path(
166 383 'pullrequest_show',
167 384 repo_name=pull_request.target_repo.scm_instance().name,
168 385 pull_request_id=pull_request.pull_request_id))
169 386
170 387 for commit_id in pull_request.revisions:
171 388 response.mustcontain(commit_id)
172 389
173 390 def test_show_invalid_source_reference(self, pr_util):
174 391 pull_request = pr_util.create_pull_request()
175 392 pull_request.source_ref = 'branch:b:invalid'
176 393 Session().add(pull_request)
177 394 Session().commit()
178 395
179 396 self.app.get(route_path(
180 397 'pullrequest_show',
181 398 repo_name=pull_request.target_repo.scm_instance().name,
182 399 pull_request_id=pull_request.pull_request_id))
183 400
184 401 def test_edit_title_description(self, pr_util, csrf_token):
185 402 pull_request = pr_util.create_pull_request()
186 403 pull_request_id = pull_request.pull_request_id
187 404
188 405 response = self.app.post(
189 406 route_path('pullrequest_update',
190 407 repo_name=pull_request.target_repo.repo_name,
191 408 pull_request_id=pull_request_id),
192 409 params={
193 410 'edit_pull_request': 'true',
194 411 'title': 'New title',
195 412 'description': 'New description',
196 413 'csrf_token': csrf_token})
197 414
198 415 assert_session_flash(
199 416 response, u'Pull request title & description updated.',
200 417 category='success')
201 418
202 419 pull_request = PullRequest.get(pull_request_id)
203 420 assert pull_request.title == 'New title'
204 421 assert pull_request.description == 'New description'
205 422
206 423 def test_edit_title_description_closed(self, pr_util, csrf_token):
207 424 pull_request = pr_util.create_pull_request()
208 425 pull_request_id = pull_request.pull_request_id
209 426 repo_name = pull_request.target_repo.repo_name
210 427 pr_util.close()
211 428
212 429 response = self.app.post(
213 430 route_path('pullrequest_update',
214 431 repo_name=repo_name, pull_request_id=pull_request_id),
215 432 params={
216 433 'edit_pull_request': 'true',
217 434 'title': 'New title',
218 435 'description': 'New description',
219 436 'csrf_token': csrf_token}, status=200)
220 437 assert_session_flash(
221 438 response, u'Cannot update closed pull requests.',
222 439 category='error')
223 440
224 441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
225 442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
226 443
227 444 pull_request = pr_util.create_pull_request()
228 445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
229 446 Session().add(pull_request)
230 447 Session().commit()
231 448
232 449 pull_request_id = pull_request.pull_request_id
233 450
234 451 response = self.app.post(
235 452 route_path('pullrequest_update',
236 453 repo_name=pull_request.target_repo.repo_name,
237 454 pull_request_id=pull_request_id),
238 455 params={'update_commits': 'true', 'csrf_token': csrf_token})
239 456
240 457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
241 458 UpdateFailureReason.MISSING_SOURCE_REF])
242 459 assert_session_flash(response, expected_msg, category='error')
243 460
244 461 def test_missing_target_reference(self, pr_util, csrf_token):
245 462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
246 463 pull_request = pr_util.create_pull_request(
247 464 approved=True, mergeable=True)
248 465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
249 466 pull_request.target_ref = unicode_reference
250 467 Session().add(pull_request)
251 468 Session().commit()
252 469
253 470 pull_request_id = pull_request.pull_request_id
254 471 pull_request_url = route_path(
255 472 'pullrequest_show',
256 473 repo_name=pull_request.target_repo.repo_name,
257 474 pull_request_id=pull_request_id)
258 475
259 476 response = self.app.get(pull_request_url)
260 477 target_ref_id = 'invalid-branch'
261 478 merge_resp = MergeResponse(
262 479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
263 480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
264 481 response.assert_response().element_contains(
265 482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
266 483
267 484 def test_comment_and_close_pull_request_custom_message_approved(
268 485 self, pr_util, csrf_token, xhr_header):
269 486
270 487 pull_request = pr_util.create_pull_request(approved=True)
271 488 pull_request_id = pull_request.pull_request_id
272 489 author = pull_request.user_id
273 490 repo = pull_request.target_repo.repo_id
274 491
275 492 self.app.post(
276 493 route_path('pullrequest_comment_create',
277 494 repo_name=pull_request.target_repo.scm_instance().name,
278 495 pull_request_id=pull_request_id),
279 496 params={
280 497 'close_pull_request': '1',
281 498 'text': 'Closing a PR',
282 499 'csrf_token': csrf_token},
283 500 extra_environ=xhr_header,)
284 501
285 502 journal = UserLog.query()\
286 503 .filter(UserLog.user_id == author)\
287 504 .filter(UserLog.repository_id == repo) \
288 505 .order_by(UserLog.user_log_id.asc()) \
289 506 .all()
290 507 assert journal[-1].action == 'repo.pull_request.close'
291 508
292 509 pull_request = PullRequest.get(pull_request_id)
293 510 assert pull_request.is_closed()
294 511
295 512 status = ChangesetStatusModel().get_status(
296 513 pull_request.source_repo, pull_request=pull_request)
297 514 assert status == ChangesetStatus.STATUS_APPROVED
298 515 comments = ChangesetComment().query() \
299 516 .filter(ChangesetComment.pull_request == pull_request) \
300 517 .order_by(ChangesetComment.comment_id.asc())\
301 518 .all()
302 519 assert comments[-1].text == 'Closing a PR'
303 520
304 521 def test_comment_force_close_pull_request_rejected(
305 522 self, pr_util, csrf_token, xhr_header):
306 523 pull_request = pr_util.create_pull_request()
307 524 pull_request_id = pull_request.pull_request_id
308 525 PullRequestModel().update_reviewers(
309 526 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
310 527 pull_request.author)
311 528 author = pull_request.user_id
312 529 repo = pull_request.target_repo.repo_id
313 530
314 531 self.app.post(
315 532 route_path('pullrequest_comment_create',
316 533 repo_name=pull_request.target_repo.scm_instance().name,
317 534 pull_request_id=pull_request_id),
318 535 params={
319 536 'close_pull_request': '1',
320 537 'csrf_token': csrf_token},
321 538 extra_environ=xhr_header)
322 539
323 540 pull_request = PullRequest.get(pull_request_id)
324 541
325 542 journal = UserLog.query()\
326 543 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
327 544 .order_by(UserLog.user_log_id.asc()) \
328 545 .all()
329 546 assert journal[-1].action == 'repo.pull_request.close'
330 547
331 548 # check only the latest status, not the review status
332 549 status = ChangesetStatusModel().get_status(
333 550 pull_request.source_repo, pull_request=pull_request)
334 551 assert status == ChangesetStatus.STATUS_REJECTED
335 552
336 553 def test_comment_and_close_pull_request(
337 554 self, pr_util, csrf_token, xhr_header):
338 555 pull_request = pr_util.create_pull_request()
339 556 pull_request_id = pull_request.pull_request_id
340 557
341 558 response = self.app.post(
342 559 route_path('pullrequest_comment_create',
343 560 repo_name=pull_request.target_repo.scm_instance().name,
344 561 pull_request_id=pull_request.pull_request_id),
345 562 params={
346 563 'close_pull_request': 'true',
347 564 'csrf_token': csrf_token},
348 565 extra_environ=xhr_header)
349 566
350 567 assert response.json
351 568
352 569 pull_request = PullRequest.get(pull_request_id)
353 570 assert pull_request.is_closed()
354 571
355 572 # check only the latest status, not the review status
356 573 status = ChangesetStatusModel().get_status(
357 574 pull_request.source_repo, pull_request=pull_request)
358 575 assert status == ChangesetStatus.STATUS_REJECTED
359 576
360 577 def test_comment_and_close_pull_request_try_edit_comment(
361 578 self, pr_util, csrf_token, xhr_header
362 579 ):
363 580 pull_request = pr_util.create_pull_request()
364 581 pull_request_id = pull_request.pull_request_id
365 582 target_scm = pull_request.target_repo.scm_instance()
366 583 target_scm_name = target_scm.name
367 584
368 585 response = self.app.post(
369 586 route_path(
370 587 'pullrequest_comment_create',
371 588 repo_name=target_scm_name,
372 589 pull_request_id=pull_request_id,
373 590 ),
374 591 params={
375 592 'close_pull_request': 'true',
376 593 'csrf_token': csrf_token,
377 594 },
378 595 extra_environ=xhr_header)
379 596
380 597 assert response.json
381 598
382 599 pull_request = PullRequest.get(pull_request_id)
383 600 target_scm = pull_request.target_repo.scm_instance()
384 601 target_scm_name = target_scm.name
385 602 assert pull_request.is_closed()
386 603
387 604 # check only the latest status, not the review status
388 605 status = ChangesetStatusModel().get_status(
389 606 pull_request.source_repo, pull_request=pull_request)
390 607 assert status == ChangesetStatus.STATUS_REJECTED
391 608
392 609 comment_id = response.json.get('comment_id', None)
393 610 test_text = 'test'
394 611 response = self.app.post(
395 612 route_path(
396 613 'pullrequest_comment_edit',
397 614 repo_name=target_scm_name,
398 615 pull_request_id=pull_request_id,
399 616 comment_id=comment_id,
400 617 ),
401 618 extra_environ=xhr_header,
402 619 params={
403 620 'csrf_token': csrf_token,
404 621 'text': test_text,
405 622 },
406 623 status=403,
407 624 )
408 625 assert response.status_int == 403
409 626
410 627 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
411 628 pull_request = pr_util.create_pull_request()
412 629 target_scm = pull_request.target_repo.scm_instance()
413 630 target_scm_name = target_scm.name
414 631
415 632 response = self.app.post(
416 633 route_path(
417 634 'pullrequest_comment_create',
418 635 repo_name=target_scm_name,
419 636 pull_request_id=pull_request.pull_request_id),
420 637 params={
421 638 'csrf_token': csrf_token,
422 639 'text': 'init',
423 640 },
424 641 extra_environ=xhr_header,
425 642 )
426 643 assert response.json
427 644
428 645 comment_id = response.json.get('comment_id', None)
429 646 assert comment_id
430 647 test_text = 'test'
431 648 self.app.post(
432 649 route_path(
433 650 'pullrequest_comment_edit',
434 651 repo_name=target_scm_name,
435 652 pull_request_id=pull_request.pull_request_id,
436 653 comment_id=comment_id,
437 654 ),
438 655 extra_environ=xhr_header,
439 656 params={
440 657 'csrf_token': csrf_token,
441 658 'text': test_text,
442 659 'version': '0',
443 660 },
444 661
445 662 )
446 663 text_form_db = ChangesetComment.query().filter(
447 664 ChangesetComment.comment_id == comment_id).first().text
448 665 assert test_text == text_form_db
449 666
450 667 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
451 668 pull_request = pr_util.create_pull_request()
452 669 target_scm = pull_request.target_repo.scm_instance()
453 670 target_scm_name = target_scm.name
454 671
455 672 response = self.app.post(
456 673 route_path(
457 674 'pullrequest_comment_create',
458 675 repo_name=target_scm_name,
459 676 pull_request_id=pull_request.pull_request_id),
460 677 params={
461 678 'csrf_token': csrf_token,
462 679 'text': 'init',
463 680 },
464 681 extra_environ=xhr_header,
465 682 )
466 683 assert response.json
467 684
468 685 comment_id = response.json.get('comment_id', None)
469 686 assert comment_id
470 687 test_text = 'init'
471 688 response = self.app.post(
472 689 route_path(
473 690 'pullrequest_comment_edit',
474 691 repo_name=target_scm_name,
475 692 pull_request_id=pull_request.pull_request_id,
476 693 comment_id=comment_id,
477 694 ),
478 695 extra_environ=xhr_header,
479 696 params={
480 697 'csrf_token': csrf_token,
481 698 'text': test_text,
482 699 'version': '0',
483 700 },
484 701 status=404,
485 702
486 703 )
487 704 assert response.status_int == 404
488 705
489 706 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
490 707 pull_request = pr_util.create_pull_request()
491 708 target_scm = pull_request.target_repo.scm_instance()
492 709 target_scm_name = target_scm.name
493 710
494 711 response = self.app.post(
495 712 route_path(
496 713 'pullrequest_comment_create',
497 714 repo_name=target_scm_name,
498 715 pull_request_id=pull_request.pull_request_id),
499 716 params={
500 717 'csrf_token': csrf_token,
501 718 'text': 'init',
502 719 },
503 720 extra_environ=xhr_header,
504 721 )
505 722 assert response.json
506 723 comment_id = response.json.get('comment_id', None)
507 724 assert comment_id
508 725
509 726 test_text = 'test'
510 727 self.app.post(
511 728 route_path(
512 729 'pullrequest_comment_edit',
513 730 repo_name=target_scm_name,
514 731 pull_request_id=pull_request.pull_request_id,
515 732 comment_id=comment_id,
516 733 ),
517 734 extra_environ=xhr_header,
518 735 params={
519 736 'csrf_token': csrf_token,
520 737 'text': test_text,
521 738 'version': '0',
522 739 },
523 740
524 741 )
525 742 test_text_v2 = 'test_v2'
526 743 response = self.app.post(
527 744 route_path(
528 745 'pullrequest_comment_edit',
529 746 repo_name=target_scm_name,
530 747 pull_request_id=pull_request.pull_request_id,
531 748 comment_id=comment_id,
532 749 ),
533 750 extra_environ=xhr_header,
534 751 params={
535 752 'csrf_token': csrf_token,
536 753 'text': test_text_v2,
537 754 'version': '0',
538 755 },
539 756 status=409,
540 757 )
541 758 assert response.status_int == 409
542 759
543 760 text_form_db = ChangesetComment.query().filter(
544 761 ChangesetComment.comment_id == comment_id).first().text
545 762
546 763 assert test_text == text_form_db
547 764 assert test_text_v2 != text_form_db
548 765
549 766 def test_comment_and_comment_edit_permissions_forbidden(
550 767 self, autologin_regular_user, user_regular, user_admin, pr_util,
551 768 csrf_token, xhr_header):
552 769 pull_request = pr_util.create_pull_request(
553 770 author=user_admin.username, enable_notifications=False)
554 771 comment = CommentsModel().create(
555 772 text='test',
556 773 repo=pull_request.target_repo.scm_instance().name,
557 774 user=user_admin,
558 775 pull_request=pull_request,
559 776 )
560 777 response = self.app.post(
561 778 route_path(
562 779 'pullrequest_comment_edit',
563 780 repo_name=pull_request.target_repo.scm_instance().name,
564 781 pull_request_id=pull_request.pull_request_id,
565 782 comment_id=comment.comment_id,
566 783 ),
567 784 extra_environ=xhr_header,
568 785 params={
569 786 'csrf_token': csrf_token,
570 787 'text': 'test_text',
571 788 },
572 789 status=403,
573 790 )
574 791 assert response.status_int == 403
575 792
576 793 def test_create_pull_request(self, backend, csrf_token):
577 794 commits = [
578 795 {'message': 'ancestor'},
579 796 {'message': 'change'},
580 797 {'message': 'change2'},
581 798 ]
582 799 commit_ids = backend.create_master_repo(commits)
583 800 target = backend.create_repo(heads=['ancestor'])
584 801 source = backend.create_repo(heads=['change2'])
585 802
586 803 response = self.app.post(
587 804 route_path('pullrequest_create', repo_name=source.repo_name),
588 805 [
589 806 ('source_repo', source.repo_name),
590 807 ('source_ref', 'branch:default:' + commit_ids['change2']),
591 808 ('target_repo', target.repo_name),
592 809 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
593 810 ('common_ancestor', commit_ids['ancestor']),
594 811 ('pullrequest_title', 'Title'),
595 812 ('pullrequest_desc', 'Description'),
596 813 ('description_renderer', 'markdown'),
597 814 ('__start__', 'review_members:sequence'),
598 815 ('__start__', 'reviewer:mapping'),
599 816 ('user_id', '1'),
600 817 ('__start__', 'reasons:sequence'),
601 818 ('reason', 'Some reason'),
602 819 ('__end__', 'reasons:sequence'),
603 820 ('__start__', 'rules:sequence'),
604 821 ('__end__', 'rules:sequence'),
605 822 ('mandatory', 'False'),
606 823 ('__end__', 'reviewer:mapping'),
607 824 ('__end__', 'review_members:sequence'),
608 825 ('__start__', 'revisions:sequence'),
609 826 ('revisions', commit_ids['change']),
610 827 ('revisions', commit_ids['change2']),
611 828 ('__end__', 'revisions:sequence'),
612 829 ('user', ''),
613 830 ('csrf_token', csrf_token),
614 831 ],
615 832 status=302)
616 833
617 834 location = response.headers['Location']
618 835 pull_request_id = location.rsplit('/', 1)[1]
619 836 assert pull_request_id != 'new'
620 837 pull_request = PullRequest.get(int(pull_request_id))
621 838
622 839 # check that we have now both revisions
623 840 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
624 841 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
625 842 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
626 843 assert pull_request.target_ref == expected_target_ref
627 844
628 845 def test_reviewer_notifications(self, backend, csrf_token):
629 846 # We have to use the app.post for this test so it will create the
630 847 # notifications properly with the new PR
631 848 commits = [
632 849 {'message': 'ancestor',
633 850 'added': [FileNode('file_A', content='content_of_ancestor')]},
634 851 {'message': 'change',
635 852 'added': [FileNode('file_a', content='content_of_change')]},
636 853 {'message': 'change-child'},
637 854 {'message': 'ancestor-child', 'parents': ['ancestor'],
638 855 'added': [
639 856 FileNode('file_B', content='content_of_ancestor_child')]},
640 857 {'message': 'ancestor-child-2'},
641 858 ]
642 859 commit_ids = backend.create_master_repo(commits)
643 860 target = backend.create_repo(heads=['ancestor-child'])
644 861 source = backend.create_repo(heads=['change'])
645 862
646 863 response = self.app.post(
647 864 route_path('pullrequest_create', repo_name=source.repo_name),
648 865 [
649 866 ('source_repo', source.repo_name),
650 867 ('source_ref', 'branch:default:' + commit_ids['change']),
651 868 ('target_repo', target.repo_name),
652 869 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
653 870 ('common_ancestor', commit_ids['ancestor']),
654 871 ('pullrequest_title', 'Title'),
655 872 ('pullrequest_desc', 'Description'),
656 873 ('description_renderer', 'markdown'),
657 874 ('__start__', 'review_members:sequence'),
658 875 ('__start__', 'reviewer:mapping'),
659 876 ('user_id', '2'),
660 877 ('__start__', 'reasons:sequence'),
661 878 ('reason', 'Some reason'),
662 879 ('__end__', 'reasons:sequence'),
663 880 ('__start__', 'rules:sequence'),
664 881 ('__end__', 'rules:sequence'),
665 882 ('mandatory', 'False'),
666 883 ('__end__', 'reviewer:mapping'),
667 884 ('__end__', 'review_members:sequence'),
668 885 ('__start__', 'revisions:sequence'),
669 886 ('revisions', commit_ids['change']),
670 887 ('__end__', 'revisions:sequence'),
671 888 ('user', ''),
672 889 ('csrf_token', csrf_token),
673 890 ],
674 891 status=302)
675 892
676 893 location = response.headers['Location']
677 894
678 895 pull_request_id = location.rsplit('/', 1)[1]
679 896 assert pull_request_id != 'new'
680 897 pull_request = PullRequest.get(int(pull_request_id))
681 898
682 899 # Check that a notification was made
683 900 notifications = Notification.query()\
684 901 .filter(Notification.created_by == pull_request.author.user_id,
685 902 Notification.type_ == Notification.TYPE_PULL_REQUEST,
686 903 Notification.subject.contains(
687 904 "requested a pull request review. !%s" % pull_request_id))
688 905 assert len(notifications.all()) == 1
689 906
690 907 # Change reviewers and check that a notification was made
691 908 PullRequestModel().update_reviewers(
692 909 pull_request.pull_request_id, [(1, [], False, [])],
693 910 pull_request.author)
694 911 assert len(notifications.all()) == 2
695 912
696 913 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
697 914 csrf_token):
698 915 commits = [
699 916 {'message': 'ancestor',
700 917 'added': [FileNode('file_A', content='content_of_ancestor')]},
701 918 {'message': 'change',
702 919 'added': [FileNode('file_a', content='content_of_change')]},
703 920 {'message': 'change-child'},
704 921 {'message': 'ancestor-child', 'parents': ['ancestor'],
705 922 'added': [
706 923 FileNode('file_B', content='content_of_ancestor_child')]},
707 924 {'message': 'ancestor-child-2'},
708 925 ]
709 926 commit_ids = backend.create_master_repo(commits)
710 927 target = backend.create_repo(heads=['ancestor-child'])
711 928 source = backend.create_repo(heads=['change'])
712 929
713 930 response = self.app.post(
714 931 route_path('pullrequest_create', repo_name=source.repo_name),
715 932 [
716 933 ('source_repo', source.repo_name),
717 934 ('source_ref', 'branch:default:' + commit_ids['change']),
718 935 ('target_repo', target.repo_name),
719 936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
720 937 ('common_ancestor', commit_ids['ancestor']),
721 938 ('pullrequest_title', 'Title'),
722 939 ('pullrequest_desc', 'Description'),
723 940 ('description_renderer', 'markdown'),
724 941 ('__start__', 'review_members:sequence'),
725 942 ('__start__', 'reviewer:mapping'),
726 943 ('user_id', '1'),
727 944 ('__start__', 'reasons:sequence'),
728 945 ('reason', 'Some reason'),
729 946 ('__end__', 'reasons:sequence'),
730 947 ('__start__', 'rules:sequence'),
731 948 ('__end__', 'rules:sequence'),
732 949 ('mandatory', 'False'),
733 950 ('__end__', 'reviewer:mapping'),
734 951 ('__end__', 'review_members:sequence'),
735 952 ('__start__', 'revisions:sequence'),
736 953 ('revisions', commit_ids['change']),
737 954 ('__end__', 'revisions:sequence'),
738 955 ('user', ''),
739 956 ('csrf_token', csrf_token),
740 957 ],
741 958 status=302)
742 959
743 960 location = response.headers['Location']
744 961
745 962 pull_request_id = location.rsplit('/', 1)[1]
746 963 assert pull_request_id != 'new'
747 964 pull_request = PullRequest.get(int(pull_request_id))
748 965
749 966 # target_ref has to point to the ancestor's commit_id in order to
750 967 # show the correct diff
751 968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
752 969 assert pull_request.target_ref == expected_target_ref
753 970
754 971 # Check generated diff contents
755 972 response = response.follow()
756 973 response.mustcontain(no=['content_of_ancestor'])
757 974 response.mustcontain(no=['content_of_ancestor-child'])
758 975 response.mustcontain('content_of_change')
759 976
760 977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
761 978 # Clear any previous calls to rcextensions
762 979 rhodecode.EXTENSIONS.calls.clear()
763 980
764 981 pull_request = pr_util.create_pull_request(
765 982 approved=True, mergeable=True)
766 983 pull_request_id = pull_request.pull_request_id
767 984 repo_name = pull_request.target_repo.scm_instance().name,
768 985
769 986 url = route_path('pullrequest_merge',
770 987 repo_name=str(repo_name[0]),
771 988 pull_request_id=pull_request_id)
772 989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
773 990
774 991 pull_request = PullRequest.get(pull_request_id)
775 992
776 993 assert response.status_int == 200
777 994 assert pull_request.is_closed()
778 995 assert_pull_request_status(
779 996 pull_request, ChangesetStatus.STATUS_APPROVED)
780 997
781 998 # Check the relevant log entries were added
782 999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
783 1000 actions = [log.action for log in user_logs]
784 1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
785 1002 expected_actions = [
786 1003 u'repo.pull_request.close',
787 1004 u'repo.pull_request.merge',
788 1005 u'repo.pull_request.comment.create'
789 1006 ]
790 1007 assert actions == expected_actions
791 1008
792 1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
793 1010 actions = [log for log in user_logs]
794 1011 assert actions[-1].action == 'user.push'
795 1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
796 1013
797 1014 # Check post_push rcextension was really executed
798 1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
799 1016 assert len(push_calls) == 1
800 1017 unused_last_call_args, last_call_kwargs = push_calls[0]
801 1018 assert last_call_kwargs['action'] == 'push'
802 1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
803 1020
804 1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
805 1022 pull_request = pr_util.create_pull_request(mergeable=False)
806 1023 pull_request_id = pull_request.pull_request_id
807 1024 pull_request = PullRequest.get(pull_request_id)
808 1025
809 1026 response = self.app.post(
810 1027 route_path('pullrequest_merge',
811 1028 repo_name=pull_request.target_repo.scm_instance().name,
812 1029 pull_request_id=pull_request.pull_request_id),
813 1030 params={'csrf_token': csrf_token}).follow()
814 1031
815 1032 assert response.status_int == 200
816 1033 response.mustcontain(
817 1034 'Merge is not currently possible because of below failed checks.')
818 1035 response.mustcontain('Server-side pull request merging is disabled.')
819 1036
820 1037 @pytest.mark.skip_backends('svn')
821 1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
822 1039 pull_request = pr_util.create_pull_request(mergeable=True)
823 1040 pull_request_id = pull_request.pull_request_id
824 1041 repo_name = pull_request.target_repo.scm_instance().name
825 1042
826 1043 response = self.app.post(
827 1044 route_path('pullrequest_merge',
828 1045 repo_name=repo_name, pull_request_id=pull_request_id),
829 1046 params={'csrf_token': csrf_token}).follow()
830 1047
831 1048 assert response.status_int == 200
832 1049
833 1050 response.mustcontain(
834 1051 'Merge is not currently possible because of below failed checks.')
835 1052 response.mustcontain('Pull request reviewer approval is pending.')
836 1053
837 1054 def test_merge_pull_request_renders_failure_reason(
838 1055 self, user_regular, csrf_token, pr_util):
839 1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
840 1057 pull_request_id = pull_request.pull_request_id
841 1058 repo_name = pull_request.target_repo.scm_instance().name
842 1059
843 1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
844 1061 MergeFailureReason.PUSH_FAILED,
845 1062 metadata={'target': 'shadow repo',
846 1063 'merge_commit': 'xxx'})
847 1064 model_patcher = mock.patch.multiple(
848 1065 PullRequestModel,
849 1066 merge_repo=mock.Mock(return_value=merge_resp),
850 1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
851 1068
852 1069 with model_patcher:
853 1070 response = self.app.post(
854 1071 route_path('pullrequest_merge',
855 1072 repo_name=repo_name,
856 1073 pull_request_id=pull_request_id),
857 1074 params={'csrf_token': csrf_token}, status=302)
858 1075
859 1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
860 1077 metadata={'target': 'shadow repo',
861 1078 'merge_commit': 'xxx'})
862 1079 assert_session_flash(response, merge_resp.merge_status_message)
863 1080
864 1081 def test_update_source_revision(self, backend, csrf_token):
865 1082 commits = [
866 1083 {'message': 'ancestor'},
867 1084 {'message': 'change'},
868 1085 {'message': 'change-2'},
869 1086 ]
870 1087 commit_ids = backend.create_master_repo(commits)
871 1088 target = backend.create_repo(heads=['ancestor'])
872 1089 source = backend.create_repo(heads=['change'])
873 1090
874 1091 # create pr from a in source to A in target
875 1092 pull_request = PullRequest()
876 1093
877 1094 pull_request.source_repo = source
878 1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
879 1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
880 1097
881 1098 pull_request.target_repo = target
882 1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
883 1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
884 1101
885 1102 pull_request.revisions = [commit_ids['change']]
886 1103 pull_request.title = u"Test"
887 1104 pull_request.description = u"Description"
888 1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
889 1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
890 1107 Session().add(pull_request)
891 1108 Session().commit()
892 1109 pull_request_id = pull_request.pull_request_id
893 1110
894 1111 # source has ancestor - change - change-2
895 1112 backend.pull_heads(source, heads=['change-2'])
896 1113
897 1114 # update PR
898 1115 self.app.post(
899 1116 route_path('pullrequest_update',
900 1117 repo_name=target.repo_name, pull_request_id=pull_request_id),
901 1118 params={'update_commits': 'true', 'csrf_token': csrf_token})
902 1119
903 1120 response = self.app.get(
904 1121 route_path('pullrequest_show',
905 1122 repo_name=target.repo_name,
906 1123 pull_request_id=pull_request.pull_request_id))
907 1124
908 1125 assert response.status_int == 200
909 1126 response.mustcontain('Pull request updated to')
910 1127 response.mustcontain('with 1 added, 0 removed commits.')
911 1128
912 1129 # check that we have now both revisions
913 1130 pull_request = PullRequest.get(pull_request_id)
914 1131 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
915 1132
916 1133 def test_update_target_revision(self, backend, csrf_token):
917 1134 commits = [
918 1135 {'message': 'ancestor'},
919 1136 {'message': 'change'},
920 1137 {'message': 'ancestor-new', 'parents': ['ancestor']},
921 1138 {'message': 'change-rebased'},
922 1139 ]
923 1140 commit_ids = backend.create_master_repo(commits)
924 1141 target = backend.create_repo(heads=['ancestor'])
925 1142 source = backend.create_repo(heads=['change'])
926 1143
927 1144 # create pr from a in source to A in target
928 1145 pull_request = PullRequest()
929 1146
930 1147 pull_request.source_repo = source
931 1148 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
932 1149 branch=backend.default_branch_name, commit_id=commit_ids['change'])
933 1150
934 1151 pull_request.target_repo = target
935 1152 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
936 1153 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
937 1154
938 1155 pull_request.revisions = [commit_ids['change']]
939 1156 pull_request.title = u"Test"
940 1157 pull_request.description = u"Description"
941 1158 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
942 1159 pull_request.pull_request_state = PullRequest.STATE_CREATED
943 1160
944 1161 Session().add(pull_request)
945 1162 Session().commit()
946 1163 pull_request_id = pull_request.pull_request_id
947 1164
948 1165 # target has ancestor - ancestor-new
949 1166 # source has ancestor - ancestor-new - change-rebased
950 1167 backend.pull_heads(target, heads=['ancestor-new'])
951 1168 backend.pull_heads(source, heads=['change-rebased'])
952 1169
953 1170 # update PR
954 1171 url = route_path('pullrequest_update',
955 1172 repo_name=target.repo_name,
956 1173 pull_request_id=pull_request_id)
957 1174 self.app.post(url,
958 1175 params={'update_commits': 'true', 'csrf_token': csrf_token},
959 1176 status=200)
960 1177
961 1178 # check that we have now both revisions
962 1179 pull_request = PullRequest.get(pull_request_id)
963 1180 assert pull_request.revisions == [commit_ids['change-rebased']]
964 1181 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
965 1182 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
966 1183
967 1184 response = self.app.get(
968 1185 route_path('pullrequest_show',
969 1186 repo_name=target.repo_name,
970 1187 pull_request_id=pull_request.pull_request_id))
971 1188 assert response.status_int == 200
972 1189 response.mustcontain('Pull request updated to')
973 1190 response.mustcontain('with 1 added, 1 removed commits.')
974 1191
975 1192 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
976 1193 backend = backend_git
977 1194 commits = [
978 1195 {'message': 'master-commit-1'},
979 1196 {'message': 'master-commit-2-change-1'},
980 1197 {'message': 'master-commit-3-change-2'},
981 1198
982 1199 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
983 1200 {'message': 'feat-commit-2'},
984 1201 ]
985 1202 commit_ids = backend.create_master_repo(commits)
986 1203 target = backend.create_repo(heads=['master-commit-3-change-2'])
987 1204 source = backend.create_repo(heads=['feat-commit-2'])
988 1205
989 1206 # create pr from a in source to A in target
990 1207 pull_request = PullRequest()
991 1208 pull_request.source_repo = source
992 1209
993 1210 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
994 1211 branch=backend.default_branch_name,
995 1212 commit_id=commit_ids['master-commit-3-change-2'])
996 1213
997 1214 pull_request.target_repo = target
998 1215 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
999 1216 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1000 1217
1001 1218 pull_request.revisions = [
1002 1219 commit_ids['feat-commit-1'],
1003 1220 commit_ids['feat-commit-2']
1004 1221 ]
1005 1222 pull_request.title = u"Test"
1006 1223 pull_request.description = u"Description"
1007 1224 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1008 1225 pull_request.pull_request_state = PullRequest.STATE_CREATED
1009 1226 Session().add(pull_request)
1010 1227 Session().commit()
1011 1228 pull_request_id = pull_request.pull_request_id
1012 1229
1013 1230 # PR is created, now we simulate a force-push into target,
1014 1231 # that drops a 2 last commits
1015 1232 vcsrepo = target.scm_instance()
1016 1233 vcsrepo.config.clear_section('hooks')
1017 1234 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1018 1235
1019 1236 # update PR
1020 1237 url = route_path('pullrequest_update',
1021 1238 repo_name=target.repo_name,
1022 1239 pull_request_id=pull_request_id)
1023 1240 self.app.post(url,
1024 1241 params={'update_commits': 'true', 'csrf_token': csrf_token},
1025 1242 status=200)
1026 1243
1027 1244 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1028 1245 assert response.status_int == 200
1029 1246 response.mustcontain('Pull request updated to')
1030 1247 response.mustcontain('with 0 added, 0 removed commits.')
1031 1248
1032 1249 def test_update_of_ancestor_reference(self, backend, csrf_token):
1033 1250 commits = [
1034 1251 {'message': 'ancestor'},
1035 1252 {'message': 'change'},
1036 1253 {'message': 'change-2'},
1037 1254 {'message': 'ancestor-new', 'parents': ['ancestor']},
1038 1255 {'message': 'change-rebased'},
1039 1256 ]
1040 1257 commit_ids = backend.create_master_repo(commits)
1041 1258 target = backend.create_repo(heads=['ancestor'])
1042 1259 source = backend.create_repo(heads=['change'])
1043 1260
1044 1261 # create pr from a in source to A in target
1045 1262 pull_request = PullRequest()
1046 1263 pull_request.source_repo = source
1047 1264
1048 1265 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1049 1266 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1050 1267 pull_request.target_repo = target
1051 1268 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1052 1269 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1053 1270 pull_request.revisions = [commit_ids['change']]
1054 1271 pull_request.title = u"Test"
1055 1272 pull_request.description = u"Description"
1056 1273 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1057 1274 pull_request.pull_request_state = PullRequest.STATE_CREATED
1058 1275 Session().add(pull_request)
1059 1276 Session().commit()
1060 1277 pull_request_id = pull_request.pull_request_id
1061 1278
1062 1279 # target has ancestor - ancestor-new
1063 1280 # source has ancestor - ancestor-new - change-rebased
1064 1281 backend.pull_heads(target, heads=['ancestor-new'])
1065 1282 backend.pull_heads(source, heads=['change-rebased'])
1066 1283
1067 1284 # update PR
1068 1285 self.app.post(
1069 1286 route_path('pullrequest_update',
1070 1287 repo_name=target.repo_name, pull_request_id=pull_request_id),
1071 1288 params={'update_commits': 'true', 'csrf_token': csrf_token},
1072 1289 status=200)
1073 1290
1074 1291 # Expect the target reference to be updated correctly
1075 1292 pull_request = PullRequest.get(pull_request_id)
1076 1293 assert pull_request.revisions == [commit_ids['change-rebased']]
1077 1294 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1078 1295 branch=backend.default_branch_name,
1079 1296 commit_id=commit_ids['ancestor-new'])
1080 1297 assert pull_request.target_ref == expected_target_ref
1081 1298
1082 1299 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1083 1300 branch_name = 'development'
1084 1301 commits = [
1085 1302 {'message': 'initial-commit'},
1086 1303 {'message': 'old-feature'},
1087 1304 {'message': 'new-feature', 'branch': branch_name},
1088 1305 ]
1089 1306 repo = backend_git.create_repo(commits)
1090 1307 repo_name = repo.repo_name
1091 1308 commit_ids = backend_git.commit_ids
1092 1309
1093 1310 pull_request = PullRequest()
1094 1311 pull_request.source_repo = repo
1095 1312 pull_request.target_repo = repo
1096 1313 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1097 1314 branch=branch_name, commit_id=commit_ids['new-feature'])
1098 1315 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1099 1316 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1100 1317 pull_request.revisions = [commit_ids['new-feature']]
1101 1318 pull_request.title = u"Test"
1102 1319 pull_request.description = u"Description"
1103 1320 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1104 1321 pull_request.pull_request_state = PullRequest.STATE_CREATED
1105 1322 Session().add(pull_request)
1106 1323 Session().commit()
1107 1324
1108 1325 pull_request_id = pull_request.pull_request_id
1109 1326
1110 1327 vcs = repo.scm_instance()
1111 1328 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1112 1329 # NOTE(marcink): run GC to ensure the commits are gone
1113 1330 vcs.run_gc()
1114 1331
1115 1332 response = self.app.get(route_path(
1116 1333 'pullrequest_show',
1117 1334 repo_name=repo_name,
1118 1335 pull_request_id=pull_request_id))
1119 1336
1120 1337 assert response.status_int == 200
1121 1338
1122 1339 response.assert_response().element_contains(
1123 1340 '#changeset_compare_view_content .alert strong',
1124 1341 'Missing commits')
1125 1342 response.assert_response().element_contains(
1126 1343 '#changeset_compare_view_content .alert',
1127 1344 'This pull request cannot be displayed, because one or more'
1128 1345 ' commits no longer exist in the source repository.')
1129 1346
1130 1347 def test_strip_commits_from_pull_request(
1131 1348 self, backend, pr_util, csrf_token):
1132 1349 commits = [
1133 1350 {'message': 'initial-commit'},
1134 1351 {'message': 'old-feature'},
1135 1352 {'message': 'new-feature', 'parents': ['initial-commit']},
1136 1353 ]
1137 1354 pull_request = pr_util.create_pull_request(
1138 1355 commits, target_head='initial-commit', source_head='new-feature',
1139 1356 revisions=['new-feature'])
1140 1357
1141 1358 vcs = pr_util.source_repository.scm_instance()
1142 1359 if backend.alias == 'git':
1143 1360 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1144 1361 else:
1145 1362 vcs.strip(pr_util.commit_ids['new-feature'])
1146 1363
1147 1364 response = self.app.get(route_path(
1148 1365 'pullrequest_show',
1149 1366 repo_name=pr_util.target_repository.repo_name,
1150 1367 pull_request_id=pull_request.pull_request_id))
1151 1368
1152 1369 assert response.status_int == 200
1153 1370
1154 1371 response.assert_response().element_contains(
1155 1372 '#changeset_compare_view_content .alert strong',
1156 1373 'Missing commits')
1157 1374 response.assert_response().element_contains(
1158 1375 '#changeset_compare_view_content .alert',
1159 1376 'This pull request cannot be displayed, because one or more'
1160 1377 ' commits no longer exist in the source repository.')
1161 1378 response.assert_response().element_contains(
1162 1379 '#update_commits',
1163 1380 'Update commits')
1164 1381
1165 1382 def test_strip_commits_and_update(
1166 1383 self, backend, pr_util, csrf_token):
1167 1384 commits = [
1168 1385 {'message': 'initial-commit'},
1169 1386 {'message': 'old-feature'},
1170 1387 {'message': 'new-feature', 'parents': ['old-feature']},
1171 1388 ]
1172 1389 pull_request = pr_util.create_pull_request(
1173 1390 commits, target_head='old-feature', source_head='new-feature',
1174 1391 revisions=['new-feature'], mergeable=True)
1175 1392
1176 1393 vcs = pr_util.source_repository.scm_instance()
1177 1394 if backend.alias == 'git':
1178 1395 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1179 1396 else:
1180 1397 vcs.strip(pr_util.commit_ids['new-feature'])
1181 1398
1182 1399 url = route_path('pullrequest_update',
1183 1400 repo_name=pull_request.target_repo.repo_name,
1184 1401 pull_request_id=pull_request.pull_request_id)
1185 1402 response = self.app.post(url,
1186 1403 params={'update_commits': 'true',
1187 1404 'csrf_token': csrf_token})
1188 1405
1189 1406 assert response.status_int == 200
1190 1407 assert response.body == '{"response": true, "redirect_url": null}'
1191 1408
1192 1409 # Make sure that after update, it won't raise 500 errors
1193 1410 response = self.app.get(route_path(
1194 1411 'pullrequest_show',
1195 1412 repo_name=pr_util.target_repository.repo_name,
1196 1413 pull_request_id=pull_request.pull_request_id))
1197 1414
1198 1415 assert response.status_int == 200
1199 1416 response.assert_response().element_contains(
1200 1417 '#changeset_compare_view_content .alert strong',
1201 1418 'Missing commits')
1202 1419
1203 1420 def test_branch_is_a_link(self, pr_util):
1204 1421 pull_request = pr_util.create_pull_request()
1205 1422 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1206 1423 pull_request.target_ref = 'branch:target:abcdef1234567890'
1207 1424 Session().add(pull_request)
1208 1425 Session().commit()
1209 1426
1210 1427 response = self.app.get(route_path(
1211 1428 'pullrequest_show',
1212 1429 repo_name=pull_request.target_repo.scm_instance().name,
1213 1430 pull_request_id=pull_request.pull_request_id))
1214 1431 assert response.status_int == 200
1215 1432
1216 1433 source = response.assert_response().get_element('.pr-source-info')
1217 1434 source_parent = source.getparent()
1218 1435 assert len(source_parent) == 1
1219 1436
1220 1437 target = response.assert_response().get_element('.pr-target-info')
1221 1438 target_parent = target.getparent()
1222 1439 assert len(target_parent) == 1
1223 1440
1224 1441 expected_origin_link = route_path(
1225 1442 'repo_commits',
1226 1443 repo_name=pull_request.source_repo.scm_instance().name,
1227 1444 params=dict(branch='origin'))
1228 1445 expected_target_link = route_path(
1229 1446 'repo_commits',
1230 1447 repo_name=pull_request.target_repo.scm_instance().name,
1231 1448 params=dict(branch='target'))
1232 1449 assert source_parent.attrib['href'] == expected_origin_link
1233 1450 assert target_parent.attrib['href'] == expected_target_link
1234 1451
1235 1452 def test_bookmark_is_not_a_link(self, pr_util):
1236 1453 pull_request = pr_util.create_pull_request()
1237 1454 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1238 1455 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1239 1456 Session().add(pull_request)
1240 1457 Session().commit()
1241 1458
1242 1459 response = self.app.get(route_path(
1243 1460 'pullrequest_show',
1244 1461 repo_name=pull_request.target_repo.scm_instance().name,
1245 1462 pull_request_id=pull_request.pull_request_id))
1246 1463 assert response.status_int == 200
1247 1464
1248 1465 source = response.assert_response().get_element('.pr-source-info')
1249 1466 assert source.text.strip() == 'bookmark:origin'
1250 1467 assert source.getparent().attrib.get('href') is None
1251 1468
1252 1469 target = response.assert_response().get_element('.pr-target-info')
1253 1470 assert target.text.strip() == 'bookmark:target'
1254 1471 assert target.getparent().attrib.get('href') is None
1255 1472
1256 1473 def test_tag_is_not_a_link(self, pr_util):
1257 1474 pull_request = pr_util.create_pull_request()
1258 1475 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1259 1476 pull_request.target_ref = 'tag:target:abcdef1234567890'
1260 1477 Session().add(pull_request)
1261 1478 Session().commit()
1262 1479
1263 1480 response = self.app.get(route_path(
1264 1481 'pullrequest_show',
1265 1482 repo_name=pull_request.target_repo.scm_instance().name,
1266 1483 pull_request_id=pull_request.pull_request_id))
1267 1484 assert response.status_int == 200
1268 1485
1269 1486 source = response.assert_response().get_element('.pr-source-info')
1270 1487 assert source.text.strip() == 'tag:origin'
1271 1488 assert source.getparent().attrib.get('href') is None
1272 1489
1273 1490 target = response.assert_response().get_element('.pr-target-info')
1274 1491 assert target.text.strip() == 'tag:target'
1275 1492 assert target.getparent().attrib.get('href') is None
1276 1493
1277 1494 @pytest.mark.parametrize('mergeable', [True, False])
1278 1495 def test_shadow_repository_link(
1279 1496 self, mergeable, pr_util, http_host_only_stub):
1280 1497 """
1281 1498 Check that the pull request summary page displays a link to the shadow
1282 1499 repository if the pull request is mergeable. If it is not mergeable
1283 1500 the link should not be displayed.
1284 1501 """
1285 1502 pull_request = pr_util.create_pull_request(
1286 1503 mergeable=mergeable, enable_notifications=False)
1287 1504 target_repo = pull_request.target_repo.scm_instance()
1288 1505 pr_id = pull_request.pull_request_id
1289 1506 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1290 1507 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1291 1508
1292 1509 response = self.app.get(route_path(
1293 1510 'pullrequest_show',
1294 1511 repo_name=target_repo.name,
1295 1512 pull_request_id=pr_id))
1296 1513
1297 1514 if mergeable:
1298 1515 response.assert_response().element_value_contains(
1299 1516 'input.pr-mergeinfo', shadow_url)
1300 1517 response.assert_response().element_value_contains(
1301 1518 'input.pr-mergeinfo ', 'pr-merge')
1302 1519 else:
1303 1520 response.assert_response().no_element_exists('.pr-mergeinfo')
1304 1521
1305 1522
1306 1523 @pytest.mark.usefixtures('app')
1307 1524 @pytest.mark.backends("git", "hg")
1308 1525 class TestPullrequestsControllerDelete(object):
1309 1526 def test_pull_request_delete_button_permissions_admin(
1310 1527 self, autologin_user, user_admin, pr_util):
1311 1528 pull_request = pr_util.create_pull_request(
1312 1529 author=user_admin.username, enable_notifications=False)
1313 1530
1314 1531 response = self.app.get(route_path(
1315 1532 'pullrequest_show',
1316 1533 repo_name=pull_request.target_repo.scm_instance().name,
1317 1534 pull_request_id=pull_request.pull_request_id))
1318 1535
1319 1536 response.mustcontain('id="delete_pullrequest"')
1320 1537 response.mustcontain('Confirm to delete this pull request')
1321 1538
1322 1539 def test_pull_request_delete_button_permissions_owner(
1323 1540 self, autologin_regular_user, user_regular, pr_util):
1324 1541 pull_request = pr_util.create_pull_request(
1325 1542 author=user_regular.username, enable_notifications=False)
1326 1543
1327 1544 response = self.app.get(route_path(
1328 1545 'pullrequest_show',
1329 1546 repo_name=pull_request.target_repo.scm_instance().name,
1330 1547 pull_request_id=pull_request.pull_request_id))
1331 1548
1332 1549 response.mustcontain('id="delete_pullrequest"')
1333 1550 response.mustcontain('Confirm to delete this pull request')
1334 1551
1335 1552 def test_pull_request_delete_button_permissions_forbidden(
1336 1553 self, autologin_regular_user, user_regular, user_admin, pr_util):
1337 1554 pull_request = pr_util.create_pull_request(
1338 1555 author=user_admin.username, enable_notifications=False)
1339 1556
1340 1557 response = self.app.get(route_path(
1341 1558 'pullrequest_show',
1342 1559 repo_name=pull_request.target_repo.scm_instance().name,
1343 1560 pull_request_id=pull_request.pull_request_id))
1344 1561 response.mustcontain(no=['id="delete_pullrequest"'])
1345 1562 response.mustcontain(no=['Confirm to delete this pull request'])
1346 1563
1347 1564 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1348 1565 self, autologin_regular_user, user_regular, user_admin, pr_util,
1349 1566 user_util):
1350 1567
1351 1568 pull_request = pr_util.create_pull_request(
1352 1569 author=user_admin.username, enable_notifications=False)
1353 1570
1354 1571 user_util.grant_user_permission_to_repo(
1355 1572 pull_request.target_repo, user_regular,
1356 1573 'repository.write')
1357 1574
1358 1575 response = self.app.get(route_path(
1359 1576 'pullrequest_show',
1360 1577 repo_name=pull_request.target_repo.scm_instance().name,
1361 1578 pull_request_id=pull_request.pull_request_id))
1362 1579
1363 1580 response.mustcontain('id="open_edit_pullrequest"')
1364 1581 response.mustcontain('id="delete_pullrequest"')
1365 1582 response.mustcontain(no=['Confirm to delete this pull request'])
1366 1583
1367 1584 def test_delete_comment_returns_404_if_comment_does_not_exist(
1368 1585 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1369 1586
1370 1587 pull_request = pr_util.create_pull_request(
1371 1588 author=user_admin.username, enable_notifications=False)
1372 1589
1373 1590 self.app.post(
1374 1591 route_path(
1375 1592 'pullrequest_comment_delete',
1376 1593 repo_name=pull_request.target_repo.scm_instance().name,
1377 1594 pull_request_id=pull_request.pull_request_id,
1378 1595 comment_id=1024404),
1379 1596 extra_environ=xhr_header,
1380 1597 params={'csrf_token': csrf_token},
1381 1598 status=404
1382 1599 )
1383 1600
1384 1601 def test_delete_comment(
1385 1602 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1386 1603
1387 1604 pull_request = pr_util.create_pull_request(
1388 1605 author=user_admin.username, enable_notifications=False)
1389 1606 comment = pr_util.create_comment()
1390 1607 comment_id = comment.comment_id
1391 1608
1392 1609 response = self.app.post(
1393 1610 route_path(
1394 1611 'pullrequest_comment_delete',
1395 1612 repo_name=pull_request.target_repo.scm_instance().name,
1396 1613 pull_request_id=pull_request.pull_request_id,
1397 1614 comment_id=comment_id),
1398 1615 extra_environ=xhr_header,
1399 1616 params={'csrf_token': csrf_token},
1400 1617 status=200
1401 1618 )
1402 1619 assert response.body == 'true'
1403 1620
1404 1621 @pytest.mark.parametrize('url_type', [
1405 1622 'pullrequest_new',
1406 1623 'pullrequest_create',
1407 1624 'pullrequest_update',
1408 1625 'pullrequest_merge',
1409 1626 ])
1410 1627 def test_pull_request_is_forbidden_on_archived_repo(
1411 1628 self, autologin_user, backend, xhr_header, user_util, url_type):
1412 1629
1413 1630 # create a temporary repo
1414 1631 source = user_util.create_repo(repo_type=backend.alias)
1415 1632 repo_name = source.repo_name
1416 1633 repo = Repository.get_by_repo_name(repo_name)
1417 1634 repo.archived = True
1418 1635 Session().commit()
1419 1636
1420 1637 response = self.app.get(
1421 1638 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1422 1639
1423 1640 msg = 'Action not supported for archived repository.'
1424 1641 assert_session_flash(response, msg)
1425 1642
1426 1643
1427 1644 def assert_pull_request_status(pull_request, expected_status):
1428 1645 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1429 1646 assert status == expected_status
1430 1647
1431 1648
1432 1649 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1433 1650 @pytest.mark.usefixtures("autologin_user")
1434 1651 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1435 1652 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,1617 +1,1626 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
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.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (
49 49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 81 statuses=statuses, offset=start, length=limit,
82 82 order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 85 opened_by=opened_by)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by,
91 91 order_dir=order_dir)
92 92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 94 statuses=statuses, opened_by=opened_by)
95 95 else:
96 96 pull_requests = PullRequestModel().get_all(
97 97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 98 statuses=statuses, offset=start, length=limit,
99 99 order_by=order_by, order_dir=order_dir)
100 100 pull_requests_total_count = PullRequestModel().count_all(
101 101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 102 opened_by=opened_by)
103 103
104 104 data = []
105 105 comments_model = CommentsModel()
106 106 for pr in pull_requests:
107 107 comments = comments_model.get_all_comments(
108 108 self.db_repo.repo_id, pull_request=pr)
109 109
110 110 data.append({
111 111 'name': _render('pullrequest_name',
112 112 pr.pull_request_id, pr.pull_request_state,
113 113 pr.work_in_progress, pr.target_repo.repo_name),
114 114 'name_raw': pr.pull_request_id,
115 115 'status': _render('pullrequest_status',
116 116 pr.calculated_review_status()),
117 117 'title': _render('pullrequest_title', pr.title, pr.description),
118 118 'description': h.escape(pr.description),
119 119 'updated_on': _render('pullrequest_updated_on',
120 120 h.datetime_to_time(pr.updated_on)),
121 121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 122 'created_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.created_on)),
124 124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 125 'state': pr.pull_request_state,
126 126 'author': _render('pullrequest_author',
127 127 pr.author.full_contact, ),
128 128 'author_raw': pr.author.full_name,
129 129 'comments': _render('pullrequest_comments', len(comments)),
130 130 'comments_raw': len(comments),
131 131 'closed': pr.is_closed(),
132 132 })
133 133
134 134 data = ({
135 135 'draw': draw,
136 136 'data': data,
137 137 'recordsTotal': pull_requests_total_count,
138 138 'recordsFiltered': pull_requests_total_count,
139 139 })
140 140 return data
141 141
142 142 @LoginRequired()
143 143 @HasRepoPermissionAnyDecorator(
144 144 'repository.read', 'repository.write', 'repository.admin')
145 145 @view_config(
146 146 route_name='pullrequest_show_all', request_method='GET',
147 147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 148 def pull_request_list(self):
149 149 c = self.load_default_context()
150 150
151 151 req_get = self.request.GET
152 152 c.source = str2bool(req_get.get('source'))
153 153 c.closed = str2bool(req_get.get('closed'))
154 154 c.my = str2bool(req_get.get('my'))
155 155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 157
158 158 c.active = 'open'
159 159 if c.my:
160 160 c.active = 'my'
161 161 if c.closed:
162 162 c.active = 'closed'
163 163 if c.awaiting_review and not c.source:
164 164 c.active = 'awaiting'
165 165 if c.source and not c.awaiting_review:
166 166 c.active = 'source'
167 167 if c.awaiting_my_review:
168 168 c.active = 'awaiting_my'
169 169
170 170 return self._get_template_context(c)
171 171
172 172 @LoginRequired()
173 173 @HasRepoPermissionAnyDecorator(
174 174 'repository.read', 'repository.write', 'repository.admin')
175 175 @view_config(
176 176 route_name='pullrequest_show_all_data', request_method='GET',
177 177 renderer='json_ext', xhr=True)
178 178 def pull_request_list_data(self):
179 179 self.load_default_context()
180 180
181 181 # additional filters
182 182 req_get = self.request.GET
183 183 source = str2bool(req_get.get('source'))
184 184 closed = str2bool(req_get.get('closed'))
185 185 my = str2bool(req_get.get('my'))
186 186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 188
189 189 filter_type = 'awaiting_review' if awaiting_review \
190 190 else 'awaiting_my_review' if awaiting_my_review \
191 191 else None
192 192
193 193 opened_by = None
194 194 if my:
195 195 opened_by = [self._rhodecode_user.user_id]
196 196
197 197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 198 if closed:
199 199 statuses = [PullRequest.STATUS_CLOSED]
200 200
201 201 data = self._get_pull_requests_list(
202 202 repo_name=self.db_repo_name, source=source,
203 203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 204
205 205 return data
206 206
207 207 def _is_diff_cache_enabled(self, target_repo):
208 208 caching_enabled = self._get_general_setting(
209 209 target_repo, 'rhodecode_diff_cache')
210 210 log.debug('Diff caching enabled: %s', caching_enabled)
211 211 return caching_enabled
212 212
213 213 def _get_diffset(self, source_repo_name, source_repo,
214 214 ancestor_commit,
215 215 source_ref_id, target_ref_id,
216 216 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 218
219 if use_ancestor:
220 # we might want to not use it for versions
219 221 target_ref_id = ancestor_commit.raw_id
222
220 223 vcs_diff = PullRequestModel().get_diff(
221 224 source_repo, source_ref_id, target_ref_id,
222 225 hide_whitespace_changes, diff_context)
223 226
224 227 diff_processor = diffs.DiffProcessor(
225 228 vcs_diff, format='newdiff', diff_limit=diff_limit,
226 229 file_limit=file_limit, show_full_diff=fulldiff)
227 230
228 231 _parsed = diff_processor.prepare()
229 232
230 233 diffset = codeblocks.DiffSet(
231 234 repo_name=self.db_repo_name,
232 235 source_repo_name=source_repo_name,
233 236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
234 237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
235 238 )
236 239 diffset = self.path_filter.render_patchset_filtered(
237 240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
238 241
239 242 return diffset
240 243
241 244 def _get_range_diffset(self, source_scm, source_repo,
242 245 commit1, commit2, diff_limit, file_limit,
243 246 fulldiff, hide_whitespace_changes, diff_context):
244 247 vcs_diff = source_scm.get_diff(
245 248 commit1, commit2,
246 249 ignore_whitespace=hide_whitespace_changes,
247 250 context=diff_context)
248 251
249 252 diff_processor = diffs.DiffProcessor(
250 253 vcs_diff, format='newdiff', diff_limit=diff_limit,
251 254 file_limit=file_limit, show_full_diff=fulldiff)
252 255
253 256 _parsed = diff_processor.prepare()
254 257
255 258 diffset = codeblocks.DiffSet(
256 259 repo_name=source_repo.repo_name,
257 260 source_node_getter=codeblocks.diffset_node_getter(commit1),
258 261 target_node_getter=codeblocks.diffset_node_getter(commit2))
259 262
260 263 diffset = self.path_filter.render_patchset_filtered(
261 264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
262 265
263 266 return diffset
264 267
265 268 @LoginRequired()
266 269 @HasRepoPermissionAnyDecorator(
267 270 'repository.read', 'repository.write', 'repository.admin')
268 271 @view_config(
269 272 route_name='pullrequest_show', request_method='GET',
270 273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
271 274 def pull_request_show(self):
272 275 _ = self.request.translate
273 276 c = self.load_default_context()
274 277
275 278 pull_request = PullRequest.get_or_404(
276 279 self.request.matchdict['pull_request_id'])
277 280 pull_request_id = pull_request.pull_request_id
278 281
279 282 c.state_progressing = pull_request.is_state_changing()
280 283
281 284 _new_state = {
282 285 'created': PullRequest.STATE_CREATED,
283 286 }.get(self.request.GET.get('force_state'))
284 287
285 288 if c.is_super_admin and _new_state:
286 289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
287 290 h.flash(
288 291 _('Pull Request state was force changed to `{}`').format(_new_state),
289 292 category='success')
290 293 Session().commit()
291 294
292 295 raise HTTPFound(h.route_path(
293 296 'pullrequest_show', repo_name=self.db_repo_name,
294 297 pull_request_id=pull_request_id))
295 298
296 299 version = self.request.GET.get('version')
297 300 from_version = self.request.GET.get('from_version') or version
298 301 merge_checks = self.request.GET.get('merge_checks')
299 302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
300 303
301 304 # fetch global flags of ignore ws or context lines
302 305 diff_context = diffs.get_diff_context(self.request)
303 306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
304 307
305 308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
306 309
307 310 (pull_request_latest,
308 311 pull_request_at_ver,
309 312 pull_request_display_obj,
310 313 at_version) = PullRequestModel().get_pr_version(
311 314 pull_request_id, version=version)
312 315 pr_closed = pull_request_latest.is_closed()
313 316
314 317 if pr_closed and (version or from_version):
315 318 # not allow to browse versions
316 319 raise HTTPFound(h.route_path(
317 320 'pullrequest_show', repo_name=self.db_repo_name,
318 321 pull_request_id=pull_request_id))
319 322
320 323 versions = pull_request_display_obj.versions()
321 324 # used to store per-commit range diffs
322 325 c.changes = collections.OrderedDict()
323 326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
324 327
325 328 c.at_version = at_version
326 329 c.at_version_num = (at_version
327 330 if at_version and at_version != 'latest'
328 331 else None)
329 332 c.at_version_pos = ChangesetComment.get_index_from_version(
330 333 c.at_version_num, versions)
331 334
332 335 (prev_pull_request_latest,
333 336 prev_pull_request_at_ver,
334 337 prev_pull_request_display_obj,
335 338 prev_at_version) = PullRequestModel().get_pr_version(
336 339 pull_request_id, version=from_version)
337 340
338 341 c.from_version = prev_at_version
339 342 c.from_version_num = (prev_at_version
340 343 if prev_at_version and prev_at_version != 'latest'
341 344 else None)
342 345 c.from_version_pos = ChangesetComment.get_index_from_version(
343 346 c.from_version_num, versions)
344 347
345 348 # define if we're in COMPARE mode or VIEW at version mode
346 349 compare = at_version != prev_at_version
347 350
348 351 # pull_requests repo_name we opened it against
349 352 # ie. target_repo must match
350 353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
351 354 raise HTTPNotFound()
352 355
353 356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
354 357 pull_request_at_ver)
355 358
356 359 c.pull_request = pull_request_display_obj
357 360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
358 361 c.pull_request_latest = pull_request_latest
359 362
360 363 if compare or (at_version and not at_version == 'latest'):
361 364 c.allowed_to_change_status = False
362 365 c.allowed_to_update = False
363 366 c.allowed_to_merge = False
364 367 c.allowed_to_delete = False
365 368 c.allowed_to_comment = False
366 369 c.allowed_to_close = False
367 370 else:
368 371 can_change_status = PullRequestModel().check_user_change_status(
369 372 pull_request_at_ver, self._rhodecode_user)
370 373 c.allowed_to_change_status = can_change_status and not pr_closed
371 374
372 375 c.allowed_to_update = PullRequestModel().check_user_update(
373 376 pull_request_latest, self._rhodecode_user) and not pr_closed
374 377 c.allowed_to_merge = PullRequestModel().check_user_merge(
375 378 pull_request_latest, self._rhodecode_user) and not pr_closed
376 379 c.allowed_to_delete = PullRequestModel().check_user_delete(
377 380 pull_request_latest, self._rhodecode_user) and not pr_closed
378 381 c.allowed_to_comment = not pr_closed
379 382 c.allowed_to_close = c.allowed_to_merge and not pr_closed
380 383
381 384 c.forbid_adding_reviewers = False
382 385 c.forbid_author_to_review = False
383 386 c.forbid_commit_author_to_review = False
384 387
385 388 if pull_request_latest.reviewer_data and \
386 389 'rules' in pull_request_latest.reviewer_data:
387 390 rules = pull_request_latest.reviewer_data['rules'] or {}
388 391 try:
389 392 c.forbid_adding_reviewers = rules.get(
390 393 'forbid_adding_reviewers')
391 394 c.forbid_author_to_review = rules.get(
392 395 'forbid_author_to_review')
393 396 c.forbid_commit_author_to_review = rules.get(
394 397 'forbid_commit_author_to_review')
395 398 except Exception:
396 399 pass
397 400
398 401 # check merge capabilities
399 402 _merge_check = MergeCheck.validate(
400 403 pull_request_latest, auth_user=self._rhodecode_user,
401 404 translator=self.request.translate,
402 405 force_shadow_repo_refresh=force_refresh)
403 406
404 407 c.pr_merge_errors = _merge_check.error_details
405 408 c.pr_merge_possible = not _merge_check.failed
406 409 c.pr_merge_message = _merge_check.merge_msg
407 410 c.pr_merge_source_commit = _merge_check.source_commit
408 411 c.pr_merge_target_commit = _merge_check.target_commit
409 412
410 413 c.pr_merge_info = MergeCheck.get_merge_conditions(
411 414 pull_request_latest, translator=self.request.translate)
412 415
413 416 c.pull_request_review_status = _merge_check.review_status
414 417 if merge_checks:
415 418 self.request.override_renderer = \
416 419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
417 420 return self._get_template_context(c)
418 421
419 422 comments_model = CommentsModel()
420 423
421 424 # reviewers and statuses
422 425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
423 426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
424 427
425 428 # GENERAL COMMENTS with versions #
426 429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
427 430 q = q.order_by(ChangesetComment.comment_id.asc())
428 431 general_comments = q
429 432
430 433 # pick comments we want to render at current version
431 434 c.comment_versions = comments_model.aggregate_comments(
432 435 general_comments, versions, c.at_version_num)
433 436 c.comments = c.comment_versions[c.at_version_num]['until']
434 437
435 438 # INLINE COMMENTS with versions #
436 439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
437 440 q = q.order_by(ChangesetComment.comment_id.asc())
438 441 inline_comments = q
439 442
440 443 c.inline_versions = comments_model.aggregate_comments(
441 444 inline_comments, versions, c.at_version_num, inline=True)
442 445
443 446 # TODOs
444 447 c.unresolved_comments = CommentsModel() \
445 448 .get_pull_request_unresolved_todos(pull_request)
446 449 c.resolved_comments = CommentsModel() \
447 450 .get_pull_request_resolved_todos(pull_request)
448 451
449 452 # inject latest version
450 453 latest_ver = PullRequest.get_pr_display_object(
451 454 pull_request_latest, pull_request_latest)
452 455
453 456 c.versions = versions + [latest_ver]
454 457
455 458 # if we use version, then do not show later comments
456 459 # than current version
457 460 display_inline_comments = collections.defaultdict(
458 461 lambda: collections.defaultdict(list))
459 462 for co in inline_comments:
460 463 if c.at_version_num:
461 464 # pick comments that are at least UPTO given version, so we
462 465 # don't render comments for higher version
463 466 should_render = co.pull_request_version_id and \
464 467 co.pull_request_version_id <= c.at_version_num
465 468 else:
466 469 # showing all, for 'latest'
467 470 should_render = True
468 471
469 472 if should_render:
470 473 display_inline_comments[co.f_path][co.line_no].append(co)
471 474
472 475 # load diff data into template context, if we use compare mode then
473 476 # diff is calculated based on changes between versions of PR
474 477
475 478 source_repo = pull_request_at_ver.source_repo
476 479 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
477 480
478 481 target_repo = pull_request_at_ver.target_repo
479 482 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
480 483
481 484 if compare:
482 485 # in compare switch the diff base to latest commit from prev version
483 486 target_ref_id = prev_pull_request_display_obj.revisions[0]
484 487
485 488 # despite opening commits for bookmarks/branches/tags, we always
486 489 # convert this to rev to prevent changes after bookmark or branch change
487 490 c.source_ref_type = 'rev'
488 491 c.source_ref = source_ref_id
489 492
490 493 c.target_ref_type = 'rev'
491 494 c.target_ref = target_ref_id
492 495
493 496 c.source_repo = source_repo
494 497 c.target_repo = target_repo
495 498
496 499 c.commit_ranges = []
497 500 source_commit = EmptyCommit()
498 501 target_commit = EmptyCommit()
499 502 c.missing_requirements = False
500 503
501 504 source_scm = source_repo.scm_instance()
502 505 target_scm = target_repo.scm_instance()
503 506
504 507 shadow_scm = None
505 508 try:
506 509 shadow_scm = pull_request_latest.get_shadow_repo()
507 510 except Exception:
508 511 log.debug('Failed to get shadow repo', exc_info=True)
509 512 # try first the existing source_repo, and then shadow
510 513 # repo if we can obtain one
511 514 commits_source_repo = source_scm
512 515 if shadow_scm:
513 516 commits_source_repo = shadow_scm
514 517
515 518 c.commits_source_repo = commits_source_repo
516 519 c.ancestor = None # set it to None, to hide it from PR view
517 520
518 521 # empty version means latest, so we keep this to prevent
519 522 # double caching
520 523 version_normalized = version or 'latest'
521 524 from_version_normalized = from_version or 'latest'
522 525
523 526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
524 527 cache_file_path = diff_cache_exist(
525 528 cache_path, 'pull_request', pull_request_id, version_normalized,
526 529 from_version_normalized, source_ref_id, target_ref_id,
527 530 hide_whitespace_changes, diff_context, c.fulldiff)
528 531
529 532 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
530 533 force_recache = self.get_recache_flag()
531 534
532 535 cached_diff = None
533 536 if caching_enabled:
534 537 cached_diff = load_cached_diff(cache_file_path)
535 538
536 539 has_proper_commit_cache = (
537 540 cached_diff and cached_diff.get('commits')
538 541 and len(cached_diff.get('commits', [])) == 5
539 542 and cached_diff.get('commits')[0]
540 543 and cached_diff.get('commits')[3])
541 544
542 545 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
543 546 diff_commit_cache = \
544 547 (ancestor_commit, commit_cache, missing_requirements,
545 548 source_commit, target_commit) = cached_diff['commits']
546 549 else:
547 550 # NOTE(marcink): we reach potentially unreachable errors when a PR has
548 551 # merge errors resulting in potentially hidden commits in the shadow repo.
549 552 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
550 553 and _merge_check.merge_response
551 554 maybe_unreachable = maybe_unreachable \
552 555 and _merge_check.merge_response.metadata.get('unresolved_files')
553 556 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
554 557 diff_commit_cache = \
555 558 (ancestor_commit, commit_cache, missing_requirements,
556 559 source_commit, target_commit) = self.get_commits(
557 560 commits_source_repo,
558 561 pull_request_at_ver,
559 562 source_commit,
560 563 source_ref_id,
561 564 source_scm,
562 565 target_commit,
563 566 target_ref_id,
564 567 target_scm,
565 568 maybe_unreachable=maybe_unreachable)
566 569
567 570 # register our commit range
568 571 for comm in commit_cache.values():
569 572 c.commit_ranges.append(comm)
570 573
571 574 c.missing_requirements = missing_requirements
572 575 c.ancestor_commit = ancestor_commit
573 576 c.statuses = source_repo.statuses(
574 577 [x.raw_id for x in c.commit_ranges])
575 578
576 579 # auto collapse if we have more than limit
577 580 collapse_limit = diffs.DiffProcessor._collapse_commits_over
578 581 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
579 582 c.compare_mode = compare
580 583
581 584 # diff_limit is the old behavior, will cut off the whole diff
582 585 # if the limit is applied otherwise will just hide the
583 586 # big files from the front-end
584 587 diff_limit = c.visual.cut_off_limit_diff
585 588 file_limit = c.visual.cut_off_limit_file
586 589
587 590 c.missing_commits = False
588 591 if (c.missing_requirements
589 592 or isinstance(source_commit, EmptyCommit)
590 593 or source_commit == target_commit):
591 594
592 595 c.missing_commits = True
593 596 else:
594 597 c.inline_comments = display_inline_comments
595 598
599 use_ancestor = True
600 if from_version_normalized != version_normalized:
601 use_ancestor = False
602
596 603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
597 604 if not force_recache and has_proper_diff_cache:
598 605 c.diffset = cached_diff['diff']
599 606 else:
600 607 c.diffset = self._get_diffset(
601 608 c.source_repo.repo_name, commits_source_repo,
602 609 c.ancestor_commit,
603 610 source_ref_id, target_ref_id,
604 611 target_commit, source_commit,
605 612 diff_limit, file_limit, c.fulldiff,
606 hide_whitespace_changes, diff_context)
613 hide_whitespace_changes, diff_context,
614 use_ancestor=use_ancestor
615 )
607 616
608 617 # save cached diff
609 618 if caching_enabled:
610 619 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
611 620
612 621 c.limited_diff = c.diffset.limited_diff
613 622
614 623 # calculate removed files that are bound to comments
615 624 comment_deleted_files = [
616 625 fname for fname in display_inline_comments
617 626 if fname not in c.diffset.file_stats]
618 627
619 628 c.deleted_files_comments = collections.defaultdict(dict)
620 629 for fname, per_line_comments in display_inline_comments.items():
621 630 if fname in comment_deleted_files:
622 631 c.deleted_files_comments[fname]['stats'] = 0
623 632 c.deleted_files_comments[fname]['comments'] = list()
624 633 for lno, comments in per_line_comments.items():
625 634 c.deleted_files_comments[fname]['comments'].extend(comments)
626 635
627 636 # maybe calculate the range diff
628 637 if c.range_diff_on:
629 638 # TODO(marcink): set whitespace/context
630 639 context_lcl = 3
631 640 ign_whitespace_lcl = False
632 641
633 642 for commit in c.commit_ranges:
634 643 commit2 = commit
635 644 commit1 = commit.first_parent
636 645
637 646 range_diff_cache_file_path = diff_cache_exist(
638 647 cache_path, 'diff', commit.raw_id,
639 648 ign_whitespace_lcl, context_lcl, c.fulldiff)
640 649
641 650 cached_diff = None
642 651 if caching_enabled:
643 652 cached_diff = load_cached_diff(range_diff_cache_file_path)
644 653
645 654 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
646 655 if not force_recache and has_proper_diff_cache:
647 656 diffset = cached_diff['diff']
648 657 else:
649 658 diffset = self._get_range_diffset(
650 659 commits_source_repo, source_repo,
651 660 commit1, commit2, diff_limit, file_limit,
652 661 c.fulldiff, ign_whitespace_lcl, context_lcl
653 662 )
654 663
655 664 # save cached diff
656 665 if caching_enabled:
657 666 cache_diff(range_diff_cache_file_path, diffset, None)
658 667
659 668 c.changes[commit.raw_id] = diffset
660 669
661 670 # this is a hack to properly display links, when creating PR, the
662 671 # compare view and others uses different notation, and
663 672 # compare_commits.mako renders links based on the target_repo.
664 673 # We need to swap that here to generate it properly on the html side
665 674 c.target_repo = c.source_repo
666 675
667 676 c.commit_statuses = ChangesetStatus.STATUSES
668 677
669 678 c.show_version_changes = not pr_closed
670 679 if c.show_version_changes:
671 680 cur_obj = pull_request_at_ver
672 681 prev_obj = prev_pull_request_at_ver
673 682
674 683 old_commit_ids = prev_obj.revisions
675 684 new_commit_ids = cur_obj.revisions
676 685 commit_changes = PullRequestModel()._calculate_commit_id_changes(
677 686 old_commit_ids, new_commit_ids)
678 687 c.commit_changes_summary = commit_changes
679 688
680 689 # calculate the diff for commits between versions
681 690 c.commit_changes = []
682 691
683 692 def mark(cs, fw):
684 693 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
685 694
686 695 for c_type, raw_id in mark(commit_changes.added, 'a') \
687 696 + mark(commit_changes.removed, 'r') \
688 697 + mark(commit_changes.common, 'c'):
689 698
690 699 if raw_id in commit_cache:
691 700 commit = commit_cache[raw_id]
692 701 else:
693 702 try:
694 703 commit = commits_source_repo.get_commit(raw_id)
695 704 except CommitDoesNotExistError:
696 705 # in case we fail extracting still use "dummy" commit
697 706 # for display in commit diff
698 707 commit = h.AttributeDict(
699 708 {'raw_id': raw_id,
700 709 'message': 'EMPTY or MISSING COMMIT'})
701 710 c.commit_changes.append([c_type, commit])
702 711
703 712 # current user review statuses for each version
704 713 c.review_versions = {}
705 714 if self._rhodecode_user.user_id in allowed_reviewers:
706 715 for co in general_comments:
707 716 if co.author.user_id == self._rhodecode_user.user_id:
708 717 status = co.status_change
709 718 if status:
710 719 _ver_pr = status[0].comment.pull_request_version_id
711 720 c.review_versions[_ver_pr] = status[0]
712 721
713 722 return self._get_template_context(c)
714 723
715 724 def get_commits(
716 725 self, commits_source_repo, pull_request_at_ver, source_commit,
717 726 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
718 727 maybe_unreachable=False):
719 728
720 729 commit_cache = collections.OrderedDict()
721 730 missing_requirements = False
722 731
723 732 try:
724 733 pre_load = ["author", "date", "message", "branch", "parents"]
725 734
726 735 pull_request_commits = pull_request_at_ver.revisions
727 736 log.debug('Loading %s commits from %s',
728 737 len(pull_request_commits), commits_source_repo)
729 738
730 739 for rev in pull_request_commits:
731 740 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
732 741 maybe_unreachable=maybe_unreachable)
733 742 commit_cache[comm.raw_id] = comm
734 743
735 744 # Order here matters, we first need to get target, and then
736 745 # the source
737 746 target_commit = commits_source_repo.get_commit(
738 747 commit_id=safe_str(target_ref_id))
739 748
740 749 source_commit = commits_source_repo.get_commit(
741 750 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
742 751 except CommitDoesNotExistError:
743 752 log.warning('Failed to get commit from `{}` repo'.format(
744 753 commits_source_repo), exc_info=True)
745 754 except RepositoryRequirementError:
746 755 log.warning('Failed to get all required data from repo', exc_info=True)
747 756 missing_requirements = True
748 757
749 758 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
750 759
751 760 try:
752 761 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
753 762 except Exception:
754 763 ancestor_commit = None
755 764
756 765 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
757 766
758 767 def assure_not_empty_repo(self):
759 768 _ = self.request.translate
760 769
761 770 try:
762 771 self.db_repo.scm_instance().get_commit()
763 772 except EmptyRepositoryError:
764 773 h.flash(h.literal(_('There are no commits yet')),
765 774 category='warning')
766 775 raise HTTPFound(
767 776 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
768 777
769 778 @LoginRequired()
770 779 @NotAnonymous()
771 780 @HasRepoPermissionAnyDecorator(
772 781 'repository.read', 'repository.write', 'repository.admin')
773 782 @view_config(
774 783 route_name='pullrequest_new', request_method='GET',
775 784 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
776 785 def pull_request_new(self):
777 786 _ = self.request.translate
778 787 c = self.load_default_context()
779 788
780 789 self.assure_not_empty_repo()
781 790 source_repo = self.db_repo
782 791
783 792 commit_id = self.request.GET.get('commit')
784 793 branch_ref = self.request.GET.get('branch')
785 794 bookmark_ref = self.request.GET.get('bookmark')
786 795
787 796 try:
788 797 source_repo_data = PullRequestModel().generate_repo_data(
789 798 source_repo, commit_id=commit_id,
790 799 branch=branch_ref, bookmark=bookmark_ref,
791 800 translator=self.request.translate)
792 801 except CommitDoesNotExistError as e:
793 802 log.exception(e)
794 803 h.flash(_('Commit does not exist'), 'error')
795 804 raise HTTPFound(
796 805 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
797 806
798 807 default_target_repo = source_repo
799 808
800 809 if source_repo.parent and c.has_origin_repo_read_perm:
801 810 parent_vcs_obj = source_repo.parent.scm_instance()
802 811 if parent_vcs_obj and not parent_vcs_obj.is_empty():
803 812 # change default if we have a parent repo
804 813 default_target_repo = source_repo.parent
805 814
806 815 target_repo_data = PullRequestModel().generate_repo_data(
807 816 default_target_repo, translator=self.request.translate)
808 817
809 818 selected_source_ref = source_repo_data['refs']['selected_ref']
810 819 title_source_ref = ''
811 820 if selected_source_ref:
812 821 title_source_ref = selected_source_ref.split(':', 2)[1]
813 822 c.default_title = PullRequestModel().generate_pullrequest_title(
814 823 source=source_repo.repo_name,
815 824 source_ref=title_source_ref,
816 825 target=default_target_repo.repo_name
817 826 )
818 827
819 828 c.default_repo_data = {
820 829 'source_repo_name': source_repo.repo_name,
821 830 'source_refs_json': json.dumps(source_repo_data),
822 831 'target_repo_name': default_target_repo.repo_name,
823 832 'target_refs_json': json.dumps(target_repo_data),
824 833 }
825 834 c.default_source_ref = selected_source_ref
826 835
827 836 return self._get_template_context(c)
828 837
829 838 @LoginRequired()
830 839 @NotAnonymous()
831 840 @HasRepoPermissionAnyDecorator(
832 841 'repository.read', 'repository.write', 'repository.admin')
833 842 @view_config(
834 843 route_name='pullrequest_repo_refs', request_method='GET',
835 844 renderer='json_ext', xhr=True)
836 845 def pull_request_repo_refs(self):
837 846 self.load_default_context()
838 847 target_repo_name = self.request.matchdict['target_repo_name']
839 848 repo = Repository.get_by_repo_name(target_repo_name)
840 849 if not repo:
841 850 raise HTTPNotFound()
842 851
843 852 target_perm = HasRepoPermissionAny(
844 853 'repository.read', 'repository.write', 'repository.admin')(
845 854 target_repo_name)
846 855 if not target_perm:
847 856 raise HTTPNotFound()
848 857
849 858 return PullRequestModel().generate_repo_data(
850 859 repo, translator=self.request.translate)
851 860
852 861 @LoginRequired()
853 862 @NotAnonymous()
854 863 @HasRepoPermissionAnyDecorator(
855 864 'repository.read', 'repository.write', 'repository.admin')
856 865 @view_config(
857 866 route_name='pullrequest_repo_targets', request_method='GET',
858 867 renderer='json_ext', xhr=True)
859 868 def pullrequest_repo_targets(self):
860 869 _ = self.request.translate
861 870 filter_query = self.request.GET.get('query')
862 871
863 872 # get the parents
864 873 parent_target_repos = []
865 874 if self.db_repo.parent:
866 875 parents_query = Repository.query() \
867 876 .order_by(func.length(Repository.repo_name)) \
868 877 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
869 878
870 879 if filter_query:
871 880 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
872 881 parents_query = parents_query.filter(
873 882 Repository.repo_name.ilike(ilike_expression))
874 883 parents = parents_query.limit(20).all()
875 884
876 885 for parent in parents:
877 886 parent_vcs_obj = parent.scm_instance()
878 887 if parent_vcs_obj and not parent_vcs_obj.is_empty():
879 888 parent_target_repos.append(parent)
880 889
881 890 # get other forks, and repo itself
882 891 query = Repository.query() \
883 892 .order_by(func.length(Repository.repo_name)) \
884 893 .filter(
885 894 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
886 895 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
887 896 ) \
888 897 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
889 898
890 899 if filter_query:
891 900 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
892 901 query = query.filter(Repository.repo_name.ilike(ilike_expression))
893 902
894 903 limit = max(20 - len(parent_target_repos), 5) # not less then 5
895 904 target_repos = query.limit(limit).all()
896 905
897 906 all_target_repos = target_repos + parent_target_repos
898 907
899 908 repos = []
900 909 # This checks permissions to the repositories
901 910 for obj in ScmModel().get_repos(all_target_repos):
902 911 repos.append({
903 912 'id': obj['name'],
904 913 'text': obj['name'],
905 914 'type': 'repo',
906 915 'repo_id': obj['dbrepo']['repo_id'],
907 916 'repo_type': obj['dbrepo']['repo_type'],
908 917 'private': obj['dbrepo']['private'],
909 918
910 919 })
911 920
912 921 data = {
913 922 'more': False,
914 923 'results': [{
915 924 'text': _('Repositories'),
916 925 'children': repos
917 926 }] if repos else []
918 927 }
919 928 return data
920 929
921 930 @LoginRequired()
922 931 @NotAnonymous()
923 932 @HasRepoPermissionAnyDecorator(
924 933 'repository.read', 'repository.write', 'repository.admin')
925 934 @CSRFRequired()
926 935 @view_config(
927 936 route_name='pullrequest_create', request_method='POST',
928 937 renderer=None)
929 938 def pull_request_create(self):
930 939 _ = self.request.translate
931 940 self.assure_not_empty_repo()
932 941 self.load_default_context()
933 942
934 943 controls = peppercorn.parse(self.request.POST.items())
935 944
936 945 try:
937 946 form = PullRequestForm(
938 947 self.request.translate, self.db_repo.repo_id)()
939 948 _form = form.to_python(controls)
940 949 except formencode.Invalid as errors:
941 950 if errors.error_dict.get('revisions'):
942 951 msg = 'Revisions: %s' % errors.error_dict['revisions']
943 952 elif errors.error_dict.get('pullrequest_title'):
944 953 msg = errors.error_dict.get('pullrequest_title')
945 954 else:
946 955 msg = _('Error creating pull request: {}').format(errors)
947 956 log.exception(msg)
948 957 h.flash(msg, 'error')
949 958
950 959 # would rather just go back to form ...
951 960 raise HTTPFound(
952 961 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
953 962
954 963 source_repo = _form['source_repo']
955 964 source_ref = _form['source_ref']
956 965 target_repo = _form['target_repo']
957 966 target_ref = _form['target_ref']
958 967 commit_ids = _form['revisions'][::-1]
959 968 common_ancestor_id = _form['common_ancestor']
960 969
961 970 # find the ancestor for this pr
962 971 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
963 972 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
964 973
965 974 if not (source_db_repo or target_db_repo):
966 975 h.flash(_('source_repo or target repo not found'), category='error')
967 976 raise HTTPFound(
968 977 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
969 978
970 979 # re-check permissions again here
971 980 # source_repo we must have read permissions
972 981
973 982 source_perm = HasRepoPermissionAny(
974 983 'repository.read', 'repository.write', 'repository.admin')(
975 984 source_db_repo.repo_name)
976 985 if not source_perm:
977 986 msg = _('Not Enough permissions to source repo `{}`.'.format(
978 987 source_db_repo.repo_name))
979 988 h.flash(msg, category='error')
980 989 # copy the args back to redirect
981 990 org_query = self.request.GET.mixed()
982 991 raise HTTPFound(
983 992 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
984 993 _query=org_query))
985 994
986 995 # target repo we must have read permissions, and also later on
987 996 # we want to check branch permissions here
988 997 target_perm = HasRepoPermissionAny(
989 998 'repository.read', 'repository.write', 'repository.admin')(
990 999 target_db_repo.repo_name)
991 1000 if not target_perm:
992 1001 msg = _('Not Enough permissions to target repo `{}`.'.format(
993 1002 target_db_repo.repo_name))
994 1003 h.flash(msg, category='error')
995 1004 # copy the args back to redirect
996 1005 org_query = self.request.GET.mixed()
997 1006 raise HTTPFound(
998 1007 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
999 1008 _query=org_query))
1000 1009
1001 1010 source_scm = source_db_repo.scm_instance()
1002 1011 target_scm = target_db_repo.scm_instance()
1003 1012
1004 1013 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1005 1014 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1006 1015
1007 1016 ancestor = source_scm.get_common_ancestor(
1008 1017 source_commit.raw_id, target_commit.raw_id, target_scm)
1009 1018
1010 1019 # recalculate target ref based on ancestor
1011 1020 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1012 1021 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1013 1022
1014 1023 get_default_reviewers_data, validate_default_reviewers = \
1015 1024 PullRequestModel().get_reviewer_functions()
1016 1025
1017 1026 # recalculate reviewers logic, to make sure we can validate this
1018 1027 reviewer_rules = get_default_reviewers_data(
1019 1028 self._rhodecode_db_user, source_db_repo,
1020 1029 source_commit, target_db_repo, target_commit)
1021 1030
1022 1031 given_reviewers = _form['review_members']
1023 1032 reviewers = validate_default_reviewers(
1024 1033 given_reviewers, reviewer_rules)
1025 1034
1026 1035 pullrequest_title = _form['pullrequest_title']
1027 1036 title_source_ref = source_ref.split(':', 2)[1]
1028 1037 if not pullrequest_title:
1029 1038 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1030 1039 source=source_repo,
1031 1040 source_ref=title_source_ref,
1032 1041 target=target_repo
1033 1042 )
1034 1043
1035 1044 description = _form['pullrequest_desc']
1036 1045 description_renderer = _form['description_renderer']
1037 1046
1038 1047 try:
1039 1048 pull_request = PullRequestModel().create(
1040 1049 created_by=self._rhodecode_user.user_id,
1041 1050 source_repo=source_repo,
1042 1051 source_ref=source_ref,
1043 1052 target_repo=target_repo,
1044 1053 target_ref=target_ref,
1045 1054 revisions=commit_ids,
1046 1055 common_ancestor_id=common_ancestor_id,
1047 1056 reviewers=reviewers,
1048 1057 title=pullrequest_title,
1049 1058 description=description,
1050 1059 description_renderer=description_renderer,
1051 1060 reviewer_data=reviewer_rules,
1052 1061 auth_user=self._rhodecode_user
1053 1062 )
1054 1063 Session().commit()
1055 1064
1056 1065 h.flash(_('Successfully opened new pull request'),
1057 1066 category='success')
1058 1067 except Exception:
1059 1068 msg = _('Error occurred during creation of this pull request.')
1060 1069 log.exception(msg)
1061 1070 h.flash(msg, category='error')
1062 1071
1063 1072 # copy the args back to redirect
1064 1073 org_query = self.request.GET.mixed()
1065 1074 raise HTTPFound(
1066 1075 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1067 1076 _query=org_query))
1068 1077
1069 1078 raise HTTPFound(
1070 1079 h.route_path('pullrequest_show', repo_name=target_repo,
1071 1080 pull_request_id=pull_request.pull_request_id))
1072 1081
1073 1082 @LoginRequired()
1074 1083 @NotAnonymous()
1075 1084 @HasRepoPermissionAnyDecorator(
1076 1085 'repository.read', 'repository.write', 'repository.admin')
1077 1086 @CSRFRequired()
1078 1087 @view_config(
1079 1088 route_name='pullrequest_update', request_method='POST',
1080 1089 renderer='json_ext')
1081 1090 def pull_request_update(self):
1082 1091 pull_request = PullRequest.get_or_404(
1083 1092 self.request.matchdict['pull_request_id'])
1084 1093 _ = self.request.translate
1085 1094
1086 1095 self.load_default_context()
1087 1096 redirect_url = None
1088 1097
1089 1098 if pull_request.is_closed():
1090 1099 log.debug('update: forbidden because pull request is closed')
1091 1100 msg = _(u'Cannot update closed pull requests.')
1092 1101 h.flash(msg, category='error')
1093 1102 return {'response': True,
1094 1103 'redirect_url': redirect_url}
1095 1104
1096 1105 is_state_changing = pull_request.is_state_changing()
1097 1106
1098 1107 # only owner or admin can update it
1099 1108 allowed_to_update = PullRequestModel().check_user_update(
1100 1109 pull_request, self._rhodecode_user)
1101 1110 if allowed_to_update:
1102 1111 controls = peppercorn.parse(self.request.POST.items())
1103 1112 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1104 1113
1105 1114 if 'review_members' in controls:
1106 1115 self._update_reviewers(
1107 1116 pull_request, controls['review_members'],
1108 1117 pull_request.reviewer_data)
1109 1118 elif str2bool(self.request.POST.get('update_commits', 'false')):
1110 1119 if is_state_changing:
1111 1120 log.debug('commits update: forbidden because pull request is in state %s',
1112 1121 pull_request.pull_request_state)
1113 1122 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1114 1123 u'Current state is: `{}`').format(
1115 1124 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1116 1125 h.flash(msg, category='error')
1117 1126 return {'response': True,
1118 1127 'redirect_url': redirect_url}
1119 1128
1120 1129 self._update_commits(pull_request)
1121 1130 if force_refresh:
1122 1131 redirect_url = h.route_path(
1123 1132 'pullrequest_show', repo_name=self.db_repo_name,
1124 1133 pull_request_id=pull_request.pull_request_id,
1125 1134 _query={"force_refresh": 1})
1126 1135 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1127 1136 self._edit_pull_request(pull_request)
1128 1137 else:
1129 1138 raise HTTPBadRequest()
1130 1139
1131 1140 return {'response': True,
1132 1141 'redirect_url': redirect_url}
1133 1142 raise HTTPForbidden()
1134 1143
1135 1144 def _edit_pull_request(self, pull_request):
1136 1145 _ = self.request.translate
1137 1146
1138 1147 try:
1139 1148 PullRequestModel().edit(
1140 1149 pull_request,
1141 1150 self.request.POST.get('title'),
1142 1151 self.request.POST.get('description'),
1143 1152 self.request.POST.get('description_renderer'),
1144 1153 self._rhodecode_user)
1145 1154 except ValueError:
1146 1155 msg = _(u'Cannot update closed pull requests.')
1147 1156 h.flash(msg, category='error')
1148 1157 return
1149 1158 else:
1150 1159 Session().commit()
1151 1160
1152 1161 msg = _(u'Pull request title & description updated.')
1153 1162 h.flash(msg, category='success')
1154 1163 return
1155 1164
1156 1165 def _update_commits(self, pull_request):
1157 1166 _ = self.request.translate
1158 1167
1159 1168 with pull_request.set_state(PullRequest.STATE_UPDATING):
1160 1169 resp = PullRequestModel().update_commits(
1161 1170 pull_request, self._rhodecode_db_user)
1162 1171
1163 1172 if resp.executed:
1164 1173
1165 1174 if resp.target_changed and resp.source_changed:
1166 1175 changed = 'target and source repositories'
1167 1176 elif resp.target_changed and not resp.source_changed:
1168 1177 changed = 'target repository'
1169 1178 elif not resp.target_changed and resp.source_changed:
1170 1179 changed = 'source repository'
1171 1180 else:
1172 1181 changed = 'nothing'
1173 1182
1174 1183 msg = _(u'Pull request updated to "{source_commit_id}" with '
1175 1184 u'{count_added} added, {count_removed} removed commits. '
1176 1185 u'Source of changes: {change_source}')
1177 1186 msg = msg.format(
1178 1187 source_commit_id=pull_request.source_ref_parts.commit_id,
1179 1188 count_added=len(resp.changes.added),
1180 1189 count_removed=len(resp.changes.removed),
1181 1190 change_source=changed)
1182 1191 h.flash(msg, category='success')
1183 1192
1184 1193 channel = '/repo${}$/pr/{}'.format(
1185 1194 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1186 1195 message = msg + (
1187 1196 ' - <a onclick="window.location.reload()">'
1188 1197 '<strong>{}</strong></a>'.format(_('Reload page')))
1189 1198 channelstream.post_message(
1190 1199 channel, message, self._rhodecode_user.username,
1191 1200 registry=self.request.registry)
1192 1201 else:
1193 1202 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1194 1203 warning_reasons = [
1195 1204 UpdateFailureReason.NO_CHANGE,
1196 1205 UpdateFailureReason.WRONG_REF_TYPE,
1197 1206 ]
1198 1207 category = 'warning' if resp.reason in warning_reasons else 'error'
1199 1208 h.flash(msg, category=category)
1200 1209
1201 1210 @LoginRequired()
1202 1211 @NotAnonymous()
1203 1212 @HasRepoPermissionAnyDecorator(
1204 1213 'repository.read', 'repository.write', 'repository.admin')
1205 1214 @CSRFRequired()
1206 1215 @view_config(
1207 1216 route_name='pullrequest_merge', request_method='POST',
1208 1217 renderer='json_ext')
1209 1218 def pull_request_merge(self):
1210 1219 """
1211 1220 Merge will perform a server-side merge of the specified
1212 1221 pull request, if the pull request is approved and mergeable.
1213 1222 After successful merging, the pull request is automatically
1214 1223 closed, with a relevant comment.
1215 1224 """
1216 1225 pull_request = PullRequest.get_or_404(
1217 1226 self.request.matchdict['pull_request_id'])
1218 1227 _ = self.request.translate
1219 1228
1220 1229 if pull_request.is_state_changing():
1221 1230 log.debug('show: forbidden because pull request is in state %s',
1222 1231 pull_request.pull_request_state)
1223 1232 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1224 1233 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1225 1234 pull_request.pull_request_state)
1226 1235 h.flash(msg, category='error')
1227 1236 raise HTTPFound(
1228 1237 h.route_path('pullrequest_show',
1229 1238 repo_name=pull_request.target_repo.repo_name,
1230 1239 pull_request_id=pull_request.pull_request_id))
1231 1240
1232 1241 self.load_default_context()
1233 1242
1234 1243 with pull_request.set_state(PullRequest.STATE_UPDATING):
1235 1244 check = MergeCheck.validate(
1236 1245 pull_request, auth_user=self._rhodecode_user,
1237 1246 translator=self.request.translate)
1238 1247 merge_possible = not check.failed
1239 1248
1240 1249 for err_type, error_msg in check.errors:
1241 1250 h.flash(error_msg, category=err_type)
1242 1251
1243 1252 if merge_possible:
1244 1253 log.debug("Pre-conditions checked, trying to merge.")
1245 1254 extras = vcs_operation_context(
1246 1255 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1247 1256 username=self._rhodecode_db_user.username, action='push',
1248 1257 scm=pull_request.target_repo.repo_type)
1249 1258 with pull_request.set_state(PullRequest.STATE_UPDATING):
1250 1259 self._merge_pull_request(
1251 1260 pull_request, self._rhodecode_db_user, extras)
1252 1261 else:
1253 1262 log.debug("Pre-conditions failed, NOT merging.")
1254 1263
1255 1264 raise HTTPFound(
1256 1265 h.route_path('pullrequest_show',
1257 1266 repo_name=pull_request.target_repo.repo_name,
1258 1267 pull_request_id=pull_request.pull_request_id))
1259 1268
1260 1269 def _merge_pull_request(self, pull_request, user, extras):
1261 1270 _ = self.request.translate
1262 1271 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1263 1272
1264 1273 if merge_resp.executed:
1265 1274 log.debug("The merge was successful, closing the pull request.")
1266 1275 PullRequestModel().close_pull_request(
1267 1276 pull_request.pull_request_id, user)
1268 1277 Session().commit()
1269 1278 msg = _('Pull request was successfully merged and closed.')
1270 1279 h.flash(msg, category='success')
1271 1280 else:
1272 1281 log.debug(
1273 1282 "The merge was not successful. Merge response: %s", merge_resp)
1274 1283 msg = merge_resp.merge_status_message
1275 1284 h.flash(msg, category='error')
1276 1285
1277 1286 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1278 1287 _ = self.request.translate
1279 1288
1280 1289 get_default_reviewers_data, validate_default_reviewers = \
1281 1290 PullRequestModel().get_reviewer_functions()
1282 1291
1283 1292 try:
1284 1293 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1285 1294 except ValueError as e:
1286 1295 log.error('Reviewers Validation: {}'.format(e))
1287 1296 h.flash(e, category='error')
1288 1297 return
1289 1298
1290 1299 old_calculated_status = pull_request.calculated_review_status()
1291 1300 PullRequestModel().update_reviewers(
1292 1301 pull_request, reviewers, self._rhodecode_user)
1293 1302 h.flash(_('Pull request reviewers updated.'), category='success')
1294 1303 Session().commit()
1295 1304
1296 1305 # trigger status changed if change in reviewers changes the status
1297 1306 calculated_status = pull_request.calculated_review_status()
1298 1307 if old_calculated_status != calculated_status:
1299 1308 PullRequestModel().trigger_pull_request_hook(
1300 1309 pull_request, self._rhodecode_user, 'review_status_change',
1301 1310 data={'status': calculated_status})
1302 1311
1303 1312 @LoginRequired()
1304 1313 @NotAnonymous()
1305 1314 @HasRepoPermissionAnyDecorator(
1306 1315 'repository.read', 'repository.write', 'repository.admin')
1307 1316 @CSRFRequired()
1308 1317 @view_config(
1309 1318 route_name='pullrequest_delete', request_method='POST',
1310 1319 renderer='json_ext')
1311 1320 def pull_request_delete(self):
1312 1321 _ = self.request.translate
1313 1322
1314 1323 pull_request = PullRequest.get_or_404(
1315 1324 self.request.matchdict['pull_request_id'])
1316 1325 self.load_default_context()
1317 1326
1318 1327 pr_closed = pull_request.is_closed()
1319 1328 allowed_to_delete = PullRequestModel().check_user_delete(
1320 1329 pull_request, self._rhodecode_user) and not pr_closed
1321 1330
1322 1331 # only owner can delete it !
1323 1332 if allowed_to_delete:
1324 1333 PullRequestModel().delete(pull_request, self._rhodecode_user)
1325 1334 Session().commit()
1326 1335 h.flash(_('Successfully deleted pull request'),
1327 1336 category='success')
1328 1337 raise HTTPFound(h.route_path('pullrequest_show_all',
1329 1338 repo_name=self.db_repo_name))
1330 1339
1331 1340 log.warning('user %s tried to delete pull request without access',
1332 1341 self._rhodecode_user)
1333 1342 raise HTTPNotFound()
1334 1343
1335 1344 @LoginRequired()
1336 1345 @NotAnonymous()
1337 1346 @HasRepoPermissionAnyDecorator(
1338 1347 'repository.read', 'repository.write', 'repository.admin')
1339 1348 @CSRFRequired()
1340 1349 @view_config(
1341 1350 route_name='pullrequest_comment_create', request_method='POST',
1342 1351 renderer='json_ext')
1343 1352 def pull_request_comment_create(self):
1344 1353 _ = self.request.translate
1345 1354
1346 1355 pull_request = PullRequest.get_or_404(
1347 1356 self.request.matchdict['pull_request_id'])
1348 1357 pull_request_id = pull_request.pull_request_id
1349 1358
1350 1359 if pull_request.is_closed():
1351 1360 log.debug('comment: forbidden because pull request is closed')
1352 1361 raise HTTPForbidden()
1353 1362
1354 1363 allowed_to_comment = PullRequestModel().check_user_comment(
1355 1364 pull_request, self._rhodecode_user)
1356 1365 if not allowed_to_comment:
1357 1366 log.debug(
1358 1367 'comment: forbidden because pull request is from forbidden repo')
1359 1368 raise HTTPForbidden()
1360 1369
1361 1370 c = self.load_default_context()
1362 1371
1363 1372 status = self.request.POST.get('changeset_status', None)
1364 1373 text = self.request.POST.get('text')
1365 1374 comment_type = self.request.POST.get('comment_type')
1366 1375 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1367 1376 close_pull_request = self.request.POST.get('close_pull_request')
1368 1377
1369 1378 # the logic here should work like following, if we submit close
1370 1379 # pr comment, use `close_pull_request_with_comment` function
1371 1380 # else handle regular comment logic
1372 1381
1373 1382 if close_pull_request:
1374 1383 # only owner or admin or person with write permissions
1375 1384 allowed_to_close = PullRequestModel().check_user_update(
1376 1385 pull_request, self._rhodecode_user)
1377 1386 if not allowed_to_close:
1378 1387 log.debug('comment: forbidden because not allowed to close '
1379 1388 'pull request %s', pull_request_id)
1380 1389 raise HTTPForbidden()
1381 1390
1382 1391 # This also triggers `review_status_change`
1383 1392 comment, status = PullRequestModel().close_pull_request_with_comment(
1384 1393 pull_request, self._rhodecode_user, self.db_repo, message=text,
1385 1394 auth_user=self._rhodecode_user)
1386 1395 Session().flush()
1387 1396
1388 1397 PullRequestModel().trigger_pull_request_hook(
1389 1398 pull_request, self._rhodecode_user, 'comment',
1390 1399 data={'comment': comment})
1391 1400
1392 1401 else:
1393 1402 # regular comment case, could be inline, or one with status.
1394 1403 # for that one we check also permissions
1395 1404
1396 1405 allowed_to_change_status = PullRequestModel().check_user_change_status(
1397 1406 pull_request, self._rhodecode_user)
1398 1407
1399 1408 if status and allowed_to_change_status:
1400 1409 message = (_('Status change %(transition_icon)s %(status)s')
1401 1410 % {'transition_icon': '>',
1402 1411 'status': ChangesetStatus.get_status_lbl(status)})
1403 1412 text = text or message
1404 1413
1405 1414 comment = CommentsModel().create(
1406 1415 text=text,
1407 1416 repo=self.db_repo.repo_id,
1408 1417 user=self._rhodecode_user.user_id,
1409 1418 pull_request=pull_request,
1410 1419 f_path=self.request.POST.get('f_path'),
1411 1420 line_no=self.request.POST.get('line'),
1412 1421 status_change=(ChangesetStatus.get_status_lbl(status)
1413 1422 if status and allowed_to_change_status else None),
1414 1423 status_change_type=(status
1415 1424 if status and allowed_to_change_status else None),
1416 1425 comment_type=comment_type,
1417 1426 resolves_comment_id=resolves_comment_id,
1418 1427 auth_user=self._rhodecode_user
1419 1428 )
1420 1429
1421 1430 if allowed_to_change_status:
1422 1431 # calculate old status before we change it
1423 1432 old_calculated_status = pull_request.calculated_review_status()
1424 1433
1425 1434 # get status if set !
1426 1435 if status:
1427 1436 ChangesetStatusModel().set_status(
1428 1437 self.db_repo.repo_id,
1429 1438 status,
1430 1439 self._rhodecode_user.user_id,
1431 1440 comment,
1432 1441 pull_request=pull_request
1433 1442 )
1434 1443
1435 1444 Session().flush()
1436 1445 # this is somehow required to get access to some relationship
1437 1446 # loaded on comment
1438 1447 Session().refresh(comment)
1439 1448
1440 1449 PullRequestModel().trigger_pull_request_hook(
1441 1450 pull_request, self._rhodecode_user, 'comment',
1442 1451 data={'comment': comment})
1443 1452
1444 1453 # we now calculate the status of pull request, and based on that
1445 1454 # calculation we set the commits status
1446 1455 calculated_status = pull_request.calculated_review_status()
1447 1456 if old_calculated_status != calculated_status:
1448 1457 PullRequestModel().trigger_pull_request_hook(
1449 1458 pull_request, self._rhodecode_user, 'review_status_change',
1450 1459 data={'status': calculated_status})
1451 1460
1452 1461 Session().commit()
1453 1462
1454 1463 data = {
1455 1464 'target_id': h.safeid(h.safe_unicode(
1456 1465 self.request.POST.get('f_path'))),
1457 1466 }
1458 1467 if comment:
1459 1468 c.co = comment
1460 1469 rendered_comment = render(
1461 1470 'rhodecode:templates/changeset/changeset_comment_block.mako',
1462 1471 self._get_template_context(c), self.request)
1463 1472
1464 1473 data.update(comment.get_dict())
1465 1474 data.update({'rendered_text': rendered_comment})
1466 1475
1467 1476 return data
1468 1477
1469 1478 @LoginRequired()
1470 1479 @NotAnonymous()
1471 1480 @HasRepoPermissionAnyDecorator(
1472 1481 'repository.read', 'repository.write', 'repository.admin')
1473 1482 @CSRFRequired()
1474 1483 @view_config(
1475 1484 route_name='pullrequest_comment_delete', request_method='POST',
1476 1485 renderer='json_ext')
1477 1486 def pull_request_comment_delete(self):
1478 1487 pull_request = PullRequest.get_or_404(
1479 1488 self.request.matchdict['pull_request_id'])
1480 1489
1481 1490 comment = ChangesetComment.get_or_404(
1482 1491 self.request.matchdict['comment_id'])
1483 1492 comment_id = comment.comment_id
1484 1493
1485 1494 if comment.immutable:
1486 1495 # don't allow deleting comments that are immutable
1487 1496 raise HTTPForbidden()
1488 1497
1489 1498 if pull_request.is_closed():
1490 1499 log.debug('comment: forbidden because pull request is closed')
1491 1500 raise HTTPForbidden()
1492 1501
1493 1502 if not comment:
1494 1503 log.debug('Comment with id:%s not found, skipping', comment_id)
1495 1504 # comment already deleted in another call probably
1496 1505 return True
1497 1506
1498 1507 if comment.pull_request.is_closed():
1499 1508 # don't allow deleting comments on closed pull request
1500 1509 raise HTTPForbidden()
1501 1510
1502 1511 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1503 1512 super_admin = h.HasPermissionAny('hg.admin')()
1504 1513 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1505 1514 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1506 1515 comment_repo_admin = is_repo_admin and is_repo_comment
1507 1516
1508 1517 if super_admin or comment_owner or comment_repo_admin:
1509 1518 old_calculated_status = comment.pull_request.calculated_review_status()
1510 1519 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1511 1520 Session().commit()
1512 1521 calculated_status = comment.pull_request.calculated_review_status()
1513 1522 if old_calculated_status != calculated_status:
1514 1523 PullRequestModel().trigger_pull_request_hook(
1515 1524 comment.pull_request, self._rhodecode_user, 'review_status_change',
1516 1525 data={'status': calculated_status})
1517 1526 return True
1518 1527 else:
1519 1528 log.warning('No permissions for user %s to delete comment_id: %s',
1520 1529 self._rhodecode_db_user, comment_id)
1521 1530 raise HTTPNotFound()
1522 1531
1523 1532 @LoginRequired()
1524 1533 @NotAnonymous()
1525 1534 @HasRepoPermissionAnyDecorator(
1526 1535 'repository.read', 'repository.write', 'repository.admin')
1527 1536 @CSRFRequired()
1528 1537 @view_config(
1529 1538 route_name='pullrequest_comment_edit', request_method='POST',
1530 1539 renderer='json_ext')
1531 1540 def pull_request_comment_edit(self):
1532 1541 self.load_default_context()
1533 1542
1534 1543 pull_request = PullRequest.get_or_404(
1535 1544 self.request.matchdict['pull_request_id']
1536 1545 )
1537 1546 comment = ChangesetComment.get_or_404(
1538 1547 self.request.matchdict['comment_id']
1539 1548 )
1540 1549 comment_id = comment.comment_id
1541 1550
1542 1551 if comment.immutable:
1543 1552 # don't allow deleting comments that are immutable
1544 1553 raise HTTPForbidden()
1545 1554
1546 1555 if pull_request.is_closed():
1547 1556 log.debug('comment: forbidden because pull request is closed')
1548 1557 raise HTTPForbidden()
1549 1558
1550 1559 if not comment:
1551 1560 log.debug('Comment with id:%s not found, skipping', comment_id)
1552 1561 # comment already deleted in another call probably
1553 1562 return True
1554 1563
1555 1564 if comment.pull_request.is_closed():
1556 1565 # don't allow deleting comments on closed pull request
1557 1566 raise HTTPForbidden()
1558 1567
1559 1568 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1560 1569 super_admin = h.HasPermissionAny('hg.admin')()
1561 1570 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1562 1571 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1563 1572 comment_repo_admin = is_repo_admin and is_repo_comment
1564 1573
1565 1574 if super_admin or comment_owner or comment_repo_admin:
1566 1575 text = self.request.POST.get('text')
1567 1576 version = self.request.POST.get('version')
1568 1577 if text == comment.text:
1569 1578 log.warning(
1570 1579 'Comment(PR): '
1571 1580 'Trying to create new version '
1572 1581 'with the same comment body {}'.format(
1573 1582 comment_id,
1574 1583 )
1575 1584 )
1576 1585 raise HTTPNotFound()
1577 1586
1578 1587 if version.isdigit():
1579 1588 version = int(version)
1580 1589 else:
1581 1590 log.warning(
1582 1591 'Comment(PR): Wrong version type {} {} '
1583 1592 'for comment {}'.format(
1584 1593 version,
1585 1594 type(version),
1586 1595 comment_id,
1587 1596 )
1588 1597 )
1589 1598 raise HTTPNotFound()
1590 1599
1591 1600 try:
1592 1601 comment_history = CommentsModel().edit(
1593 1602 comment_id=comment_id,
1594 1603 text=text,
1595 1604 auth_user=self._rhodecode_user,
1596 1605 version=version,
1597 1606 )
1598 1607 except CommentVersionMismatch:
1599 1608 raise HTTPConflict()
1600 1609
1601 1610 if not comment_history:
1602 1611 raise HTTPNotFound()
1603 1612
1604 1613 Session().commit()
1605 1614 return {
1606 1615 'comment_history_id': comment_history.comment_history_id,
1607 1616 'comment_id': comment.comment_id,
1608 1617 'comment_version': comment_history.version,
1609 1618 'comment_author_username': comment_history.author.username,
1610 1619 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1611 1620 'comment_created_on': h.age_component(comment_history.created_on,
1612 1621 time_is_local=True),
1613 1622 }
1614 1623 else:
1615 1624 log.warning('No permissions for user %s to edit comment_id: %s',
1616 1625 self._rhodecode_db_user, comment_id)
1617 1626 raise HTTPNotFound()
@@ -1,1831 +1,1833 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 collections
22 22 import datetime
23 23 import hashlib
24 24 import os
25 25 import re
26 26 import pprint
27 27 import shutil
28 28 import socket
29 29 import subprocess32
30 30 import time
31 31 import uuid
32 32 import dateutil.tz
33 33
34 34 import mock
35 35 import pyramid.testing
36 36 import pytest
37 37 import colander
38 38 import requests
39 39 import pyramid.paster
40 40
41 41 import rhodecode
42 42 from rhodecode.lib.utils2 import AttributeDict
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (
46 46 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
47 47 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.repo_group import RepoGroupModel
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54 from rhodecode.model.user_group import UserGroupModel
55 55 from rhodecode.model.integration import IntegrationModel
56 56 from rhodecode.integrations import integration_type_registry
57 57 from rhodecode.integrations.types.base import IntegrationTypeBase
58 58 from rhodecode.lib.utils import repo2db_mapper
59 59 from rhodecode.lib.vcs.backends import get_backend
60 60 from rhodecode.lib.vcs.nodes import FileNode
61 61 from rhodecode.tests import (
62 62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
63 63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
64 64 TEST_USER_REGULAR_PASS)
65 65 from rhodecode.tests.utils import CustomTestApp, set_anonymous_access
66 66 from rhodecode.tests.fixture import Fixture
67 67 from rhodecode.config import utils as config_utils
68 68
69 69
70 70 def _split_comma(value):
71 71 return value.split(',')
72 72
73 73
74 74 def pytest_addoption(parser):
75 75 parser.addoption(
76 76 '--keep-tmp-path', action='store_true',
77 77 help="Keep the test temporary directories")
78 78 parser.addoption(
79 79 '--backends', action='store', type=_split_comma,
80 80 default=['git', 'hg', 'svn'],
81 81 help="Select which backends to test for backend specific tests.")
82 82 parser.addoption(
83 83 '--dbs', action='store', type=_split_comma,
84 84 default=['sqlite'],
85 85 help="Select which database to test for database specific tests. "
86 86 "Possible options are sqlite,postgres,mysql")
87 87 parser.addoption(
88 88 '--appenlight', '--ae', action='store_true',
89 89 help="Track statistics in appenlight.")
90 90 parser.addoption(
91 91 '--appenlight-api-key', '--ae-key',
92 92 help="API key for Appenlight.")
93 93 parser.addoption(
94 94 '--appenlight-url', '--ae-url',
95 95 default="https://ae.rhodecode.com",
96 96 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
97 97 parser.addoption(
98 98 '--sqlite-connection-string', action='store',
99 99 default='', help="Connection string for the dbs tests with SQLite")
100 100 parser.addoption(
101 101 '--postgres-connection-string', action='store',
102 102 default='', help="Connection string for the dbs tests with Postgres")
103 103 parser.addoption(
104 104 '--mysql-connection-string', action='store',
105 105 default='', help="Connection string for the dbs tests with MySQL")
106 106 parser.addoption(
107 107 '--repeat', type=int, default=100,
108 108 help="Number of repetitions in performance tests.")
109 109
110 110
111 111 def pytest_configure(config):
112 112 from rhodecode.config import patches
113 113
114 114
115 115 def pytest_collection_modifyitems(session, config, items):
116 116 # nottest marked, compare nose, used for transition from nose to pytest
117 117 remaining = [
118 118 i for i in items if getattr(i.obj, '__test__', True)]
119 119 items[:] = remaining
120 120
121 121 # NOTE(marcink): custom test ordering, db tests and vcstests are slowes and should
122 122 # be executed at the end for faster test feedback
123 123 def sorter(item):
124 124 pos = 0
125 125 key = item._nodeid
126 126 if key.startswith('rhodecode/tests/database'):
127 127 pos = 1
128 128 elif key.startswith('rhodecode/tests/vcs_operations'):
129 129 pos = 2
130 130
131 131 return pos
132 132
133 133 items.sort(key=sorter)
134 134
135 135
136 136 def pytest_generate_tests(metafunc):
137 137
138 138 # Support test generation based on --backend parameter
139 139 if 'backend_alias' in metafunc.fixturenames:
140 140 backends = get_backends_from_metafunc(metafunc)
141 141 scope = None
142 142 if not backends:
143 143 pytest.skip("Not enabled for any of selected backends")
144 144
145 145 metafunc.parametrize('backend_alias', backends, scope=scope)
146 146
147 147 backend_mark = metafunc.definition.get_closest_marker('backends')
148 148 if backend_mark:
149 149 backends = get_backends_from_metafunc(metafunc)
150 150 if not backends:
151 151 pytest.skip("Not enabled for any of selected backends")
152 152
153 153
154 154 def get_backends_from_metafunc(metafunc):
155 155 requested_backends = set(metafunc.config.getoption('--backends'))
156 156 backend_mark = metafunc.definition.get_closest_marker('backends')
157 157 if backend_mark:
158 158 # Supported backends by this test function, created from
159 159 # pytest.mark.backends
160 160 backends = backend_mark.args
161 161 elif hasattr(metafunc.cls, 'backend_alias'):
162 162 # Support class attribute "backend_alias", this is mainly
163 163 # for legacy reasons for tests not yet using pytest.mark.backends
164 164 backends = [metafunc.cls.backend_alias]
165 165 else:
166 166 backends = metafunc.config.getoption('--backends')
167 167 return requested_backends.intersection(backends)
168 168
169 169
170 170 @pytest.fixture(scope='session', autouse=True)
171 171 def activate_example_rcextensions(request):
172 172 """
173 173 Patch in an example rcextensions module which verifies passed in kwargs.
174 174 """
175 175 from rhodecode.config import rcextensions
176 176
177 177 old_extensions = rhodecode.EXTENSIONS
178 178 rhodecode.EXTENSIONS = rcextensions
179 179 rhodecode.EXTENSIONS.calls = collections.defaultdict(list)
180 180
181 181 @request.addfinalizer
182 182 def cleanup():
183 183 rhodecode.EXTENSIONS = old_extensions
184 184
185 185
186 186 @pytest.fixture()
187 187 def capture_rcextensions():
188 188 """
189 189 Returns the recorded calls to entry points in rcextensions.
190 190 """
191 191 calls = rhodecode.EXTENSIONS.calls
192 192 calls.clear()
193 193 # Note: At this moment, it is still the empty dict, but that will
194 194 # be filled during the test run and since it is a reference this
195 195 # is enough to make it work.
196 196 return calls
197 197
198 198
199 199 @pytest.fixture(scope='session')
200 200 def http_environ_session():
201 201 """
202 202 Allow to use "http_environ" in session scope.
203 203 """
204 204 return plain_http_environ()
205 205
206 206
207 207 def plain_http_host_stub():
208 208 """
209 209 Value of HTTP_HOST in the test run.
210 210 """
211 211 return 'example.com:80'
212 212
213 213
214 214 @pytest.fixture()
215 215 def http_host_stub():
216 216 """
217 217 Value of HTTP_HOST in the test run.
218 218 """
219 219 return plain_http_host_stub()
220 220
221 221
222 222 def plain_http_host_only_stub():
223 223 """
224 224 Value of HTTP_HOST in the test run.
225 225 """
226 226 return plain_http_host_stub().split(':')[0]
227 227
228 228
229 229 @pytest.fixture()
230 230 def http_host_only_stub():
231 231 """
232 232 Value of HTTP_HOST in the test run.
233 233 """
234 234 return plain_http_host_only_stub()
235 235
236 236
237 237 def plain_http_environ():
238 238 """
239 239 HTTP extra environ keys.
240 240
241 241 User by the test application and as well for setting up the pylons
242 242 environment. In the case of the fixture "app" it should be possible
243 243 to override this for a specific test case.
244 244 """
245 245 return {
246 246 'SERVER_NAME': plain_http_host_only_stub(),
247 247 'SERVER_PORT': plain_http_host_stub().split(':')[1],
248 248 'HTTP_HOST': plain_http_host_stub(),
249 249 'HTTP_USER_AGENT': 'rc-test-agent',
250 250 'REQUEST_METHOD': 'GET'
251 251 }
252 252
253 253
254 254 @pytest.fixture()
255 255 def http_environ():
256 256 """
257 257 HTTP extra environ keys.
258 258
259 259 User by the test application and as well for setting up the pylons
260 260 environment. In the case of the fixture "app" it should be possible
261 261 to override this for a specific test case.
262 262 """
263 263 return plain_http_environ()
264 264
265 265
266 266 @pytest.fixture(scope='session')
267 267 def baseapp(ini_config, vcsserver, http_environ_session):
268 268 from rhodecode.lib.pyramid_utils import get_app_config
269 269 from rhodecode.config.middleware import make_pyramid_app
270 270
271 271 print("Using the RhodeCode configuration:{}".format(ini_config))
272 272 pyramid.paster.setup_logging(ini_config)
273 273
274 274 settings = get_app_config(ini_config)
275 275 app = make_pyramid_app({'__file__': ini_config}, **settings)
276 276
277 277 return app
278 278
279 279
280 280 @pytest.fixture(scope='function')
281 281 def app(request, config_stub, baseapp, http_environ):
282 282 app = CustomTestApp(
283 283 baseapp,
284 284 extra_environ=http_environ)
285 285 if request.cls:
286 286 request.cls.app = app
287 287 return app
288 288
289 289
290 290 @pytest.fixture(scope='session')
291 291 def app_settings(baseapp, ini_config):
292 292 """
293 293 Settings dictionary used to create the app.
294 294
295 295 Parses the ini file and passes the result through the sanitize and apply
296 296 defaults mechanism in `rhodecode.config.middleware`.
297 297 """
298 298 return baseapp.config.get_settings()
299 299
300 300
301 301 @pytest.fixture(scope='session')
302 302 def db_connection(ini_settings):
303 303 # Initialize the database connection.
304 304 config_utils.initialize_database(ini_settings)
305 305
306 306
307 307 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
308 308
309 309
310 310 def _autologin_user(app, *args):
311 311 session = login_user_session(app, *args)
312 312 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
313 313 return LoginData(csrf_token, session['rhodecode_user'])
314 314
315 315
316 316 @pytest.fixture()
317 317 def autologin_user(app):
318 318 """
319 319 Utility fixture which makes sure that the admin user is logged in
320 320 """
321 321 return _autologin_user(app)
322 322
323 323
324 324 @pytest.fixture()
325 325 def autologin_regular_user(app):
326 326 """
327 327 Utility fixture which makes sure that the regular user is logged in
328 328 """
329 329 return _autologin_user(
330 330 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
331 331
332 332
333 333 @pytest.fixture(scope='function')
334 334 def csrf_token(request, autologin_user):
335 335 return autologin_user.csrf_token
336 336
337 337
338 338 @pytest.fixture(scope='function')
339 339 def xhr_header(request):
340 340 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
341 341
342 342
343 343 @pytest.fixture()
344 344 def real_crypto_backend(monkeypatch):
345 345 """
346 346 Switch the production crypto backend on for this test.
347 347
348 348 During the test run the crypto backend is replaced with a faster
349 349 implementation based on the MD5 algorithm.
350 350 """
351 351 monkeypatch.setattr(rhodecode, 'is_test', False)
352 352
353 353
354 354 @pytest.fixture(scope='class')
355 355 def index_location(request, baseapp):
356 356 index_location = baseapp.config.get_settings()['search.location']
357 357 if request.cls:
358 358 request.cls.index_location = index_location
359 359 return index_location
360 360
361 361
362 362 @pytest.fixture(scope='session', autouse=True)
363 363 def tests_tmp_path(request):
364 364 """
365 365 Create temporary directory to be used during the test session.
366 366 """
367 367 if not os.path.exists(TESTS_TMP_PATH):
368 368 os.makedirs(TESTS_TMP_PATH)
369 369
370 370 if not request.config.getoption('--keep-tmp-path'):
371 371 @request.addfinalizer
372 372 def remove_tmp_path():
373 373 shutil.rmtree(TESTS_TMP_PATH)
374 374
375 375 return TESTS_TMP_PATH
376 376
377 377
378 378 @pytest.fixture()
379 379 def test_repo_group(request):
380 380 """
381 381 Create a temporary repository group, and destroy it after
382 382 usage automatically
383 383 """
384 384 fixture = Fixture()
385 385 repogroupid = 'test_repo_group_%s' % str(time.time()).replace('.', '')
386 386 repo_group = fixture.create_repo_group(repogroupid)
387 387
388 388 def _cleanup():
389 389 fixture.destroy_repo_group(repogroupid)
390 390
391 391 request.addfinalizer(_cleanup)
392 392 return repo_group
393 393
394 394
395 395 @pytest.fixture()
396 396 def test_user_group(request):
397 397 """
398 398 Create a temporary user group, and destroy it after
399 399 usage automatically
400 400 """
401 401 fixture = Fixture()
402 402 usergroupid = 'test_user_group_%s' % str(time.time()).replace('.', '')
403 403 user_group = fixture.create_user_group(usergroupid)
404 404
405 405 def _cleanup():
406 406 fixture.destroy_user_group(user_group)
407 407
408 408 request.addfinalizer(_cleanup)
409 409 return user_group
410 410
411 411
412 412 @pytest.fixture(scope='session')
413 413 def test_repo(request):
414 414 container = TestRepoContainer()
415 415 request.addfinalizer(container._cleanup)
416 416 return container
417 417
418 418
419 419 class TestRepoContainer(object):
420 420 """
421 421 Container for test repositories which are used read only.
422 422
423 423 Repositories will be created on demand and re-used during the lifetime
424 424 of this object.
425 425
426 426 Usage to get the svn test repository "minimal"::
427 427
428 428 test_repo = TestContainer()
429 429 repo = test_repo('minimal', 'svn')
430 430
431 431 """
432 432
433 433 dump_extractors = {
434 434 'git': utils.extract_git_repo_from_dump,
435 435 'hg': utils.extract_hg_repo_from_dump,
436 436 'svn': utils.extract_svn_repo_from_dump,
437 437 }
438 438
439 439 def __init__(self):
440 440 self._cleanup_repos = []
441 441 self._fixture = Fixture()
442 442 self._repos = {}
443 443
444 444 def __call__(self, dump_name, backend_alias, config=None):
445 445 key = (dump_name, backend_alias)
446 446 if key not in self._repos:
447 447 repo = self._create_repo(dump_name, backend_alias, config)
448 448 self._repos[key] = repo.repo_id
449 449 return Repository.get(self._repos[key])
450 450
451 451 def _create_repo(self, dump_name, backend_alias, config):
452 452 repo_name = '%s-%s' % (backend_alias, dump_name)
453 453 backend = get_backend(backend_alias)
454 454 dump_extractor = self.dump_extractors[backend_alias]
455 455 repo_path = dump_extractor(dump_name, repo_name)
456 456
457 457 vcs_repo = backend(repo_path, config=config)
458 458 repo2db_mapper({repo_name: vcs_repo})
459 459
460 460 repo = RepoModel().get_by_repo_name(repo_name)
461 461 self._cleanup_repos.append(repo_name)
462 462 return repo
463 463
464 464 def _cleanup(self):
465 465 for repo_name in reversed(self._cleanup_repos):
466 466 self._fixture.destroy_repo(repo_name)
467 467
468 468
469 469 def backend_base(request, backend_alias, baseapp, test_repo):
470 470 if backend_alias not in request.config.getoption('--backends'):
471 471 pytest.skip("Backend %s not selected." % (backend_alias, ))
472 472
473 473 utils.check_xfail_backends(request.node, backend_alias)
474 474 utils.check_skip_backends(request.node, backend_alias)
475 475
476 476 repo_name = 'vcs_test_%s' % (backend_alias, )
477 477 backend = Backend(
478 478 alias=backend_alias,
479 479 repo_name=repo_name,
480 480 test_name=request.node.name,
481 481 test_repo_container=test_repo)
482 482 request.addfinalizer(backend.cleanup)
483 483 return backend
484 484
485 485
486 486 @pytest.fixture()
487 487 def backend(request, backend_alias, baseapp, test_repo):
488 488 """
489 489 Parametrized fixture which represents a single backend implementation.
490 490
491 491 It respects the option `--backends` to focus the test run on specific
492 492 backend implementations.
493 493
494 494 It also supports `pytest.mark.xfail_backends` to mark tests as failing
495 495 for specific backends. This is intended as a utility for incremental
496 496 development of a new backend implementation.
497 497 """
498 498 return backend_base(request, backend_alias, baseapp, test_repo)
499 499
500 500
501 501 @pytest.fixture()
502 502 def backend_git(request, baseapp, test_repo):
503 503 return backend_base(request, 'git', baseapp, test_repo)
504 504
505 505
506 506 @pytest.fixture()
507 507 def backend_hg(request, baseapp, test_repo):
508 508 return backend_base(request, 'hg', baseapp, test_repo)
509 509
510 510
511 511 @pytest.fixture()
512 512 def backend_svn(request, baseapp, test_repo):
513 513 return backend_base(request, 'svn', baseapp, test_repo)
514 514
515 515
516 516 @pytest.fixture()
517 517 def backend_random(backend_git):
518 518 """
519 519 Use this to express that your tests need "a backend.
520 520
521 521 A few of our tests need a backend, so that we can run the code. This
522 522 fixture is intended to be used for such cases. It will pick one of the
523 523 backends and run the tests.
524 524
525 525 The fixture `backend` would run the test multiple times for each
526 526 available backend which is a pure waste of time if the test is
527 527 independent of the backend type.
528 528 """
529 529 # TODO: johbo: Change this to pick a random backend
530 530 return backend_git
531 531
532 532
533 533 @pytest.fixture()
534 534 def backend_stub(backend_git):
535 535 """
536 536 Use this to express that your tests need a backend stub
537 537
538 538 TODO: mikhail: Implement a real stub logic instead of returning
539 539 a git backend
540 540 """
541 541 return backend_git
542 542
543 543
544 544 @pytest.fixture()
545 545 def repo_stub(backend_stub):
546 546 """
547 547 Use this to express that your tests need a repository stub
548 548 """
549 549 return backend_stub.create_repo()
550 550
551 551
552 552 class Backend(object):
553 553 """
554 554 Represents the test configuration for one supported backend
555 555
556 556 Provides easy access to different test repositories based on
557 557 `__getitem__`. Such repositories will only be created once per test
558 558 session.
559 559 """
560 560
561 561 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
562 562 _master_repo = None
563 _master_repo_path = ''
563 564 _commit_ids = {}
564 565
565 566 def __init__(self, alias, repo_name, test_name, test_repo_container):
566 567 self.alias = alias
567 568 self.repo_name = repo_name
568 569 self._cleanup_repos = []
569 570 self._test_name = test_name
570 571 self._test_repo_container = test_repo_container
571 572 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
572 573 # Fixture will survive in the end.
573 574 self._fixture = Fixture()
574 575
575 576 def __getitem__(self, key):
576 577 return self._test_repo_container(key, self.alias)
577 578
578 579 def create_test_repo(self, key, config=None):
579 580 return self._test_repo_container(key, self.alias, config)
580 581
581 582 @property
582 583 def repo(self):
583 584 """
584 585 Returns the "current" repository. This is the vcs_test repo or the
585 586 last repo which has been created with `create_repo`.
586 587 """
587 588 from rhodecode.model.db import Repository
588 589 return Repository.get_by_repo_name(self.repo_name)
589 590
590 591 @property
591 592 def default_branch_name(self):
592 593 VcsRepository = get_backend(self.alias)
593 594 return VcsRepository.DEFAULT_BRANCH_NAME
594 595
595 596 @property
596 597 def default_head_id(self):
597 598 """
598 599 Returns the default head id of the underlying backend.
599 600
600 601 This will be the default branch name in case the backend does have a
601 602 default branch. In the other cases it will point to a valid head
602 603 which can serve as the base to create a new commit on top of it.
603 604 """
604 605 vcsrepo = self.repo.scm_instance()
605 606 head_id = (
606 607 vcsrepo.DEFAULT_BRANCH_NAME or
607 608 vcsrepo.commit_ids[-1])
608 609 return head_id
609 610
610 611 @property
611 612 def commit_ids(self):
612 613 """
613 614 Returns the list of commits for the last created repository
614 615 """
615 616 return self._commit_ids
616 617
617 618 def create_master_repo(self, commits):
618 619 """
619 620 Create a repository and remember it as a template.
620 621
621 622 This allows to easily create derived repositories to construct
622 623 more complex scenarios for diff, compare and pull requests.
623 624
624 625 Returns a commit map which maps from commit message to raw_id.
625 626 """
626 627 self._master_repo = self.create_repo(commits=commits)
628 self._master_repo_path = self._master_repo.repo_full_path
629
627 630 return self._commit_ids
628 631
629 632 def create_repo(
630 633 self, commits=None, number_of_commits=0, heads=None,
631 634 name_suffix=u'', bare=False, **kwargs):
632 635 """
633 636 Create a repository and record it for later cleanup.
634 637
635 638 :param commits: Optional. A sequence of dict instances.
636 639 Will add a commit per entry to the new repository.
637 640 :param number_of_commits: Optional. If set to a number, this number of
638 641 commits will be added to the new repository.
639 642 :param heads: Optional. Can be set to a sequence of of commit
640 643 names which shall be pulled in from the master repository.
641 644 :param name_suffix: adds special suffix to generated repo name
642 645 :param bare: set a repo as bare (no checkout)
643 646 """
644 647 self.repo_name = self._next_repo_name() + name_suffix
645 648 repo = self._fixture.create_repo(
646 649 self.repo_name, repo_type=self.alias, bare=bare, **kwargs)
647 650 self._cleanup_repos.append(repo.repo_name)
648 651
649 652 commits = commits or [
650 653 {'message': 'Commit %s of %s' % (x, self.repo_name)}
651 654 for x in range(number_of_commits)]
652 655 vcs_repo = repo.scm_instance()
653 656 vcs_repo.count()
654 657 self._add_commits_to_repo(vcs_repo, commits)
655 658 if heads:
656 659 self.pull_heads(repo, heads)
657 660
658 661 return repo
659 662
660 663 def pull_heads(self, repo, heads):
661 664 """
662 665 Make sure that repo contains all commits mentioned in `heads`
663 666 """
664 vcsmaster = self._master_repo.scm_instance()
665 667 vcsrepo = repo.scm_instance()
666 668 vcsrepo.config.clear_section('hooks')
667 669 commit_ids = [self._commit_ids[h] for h in heads]
668 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
670 vcsrepo.pull(self._master_repo_path, commit_ids=commit_ids)
669 671
670 672 def create_fork(self):
671 673 repo_to_fork = self.repo_name
672 674 self.repo_name = self._next_repo_name()
673 675 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
674 676 self._cleanup_repos.append(self.repo_name)
675 677 return repo
676 678
677 679 def new_repo_name(self, suffix=u''):
678 680 self.repo_name = self._next_repo_name() + suffix
679 681 self._cleanup_repos.append(self.repo_name)
680 682 return self.repo_name
681 683
682 684 def _next_repo_name(self):
683 685 return u"%s_%s" % (
684 686 self.invalid_repo_name.sub(u'_', self._test_name), len(self._cleanup_repos))
685 687
686 688 def ensure_file(self, filename, content='Test content\n'):
687 689 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
688 690 commits = [
689 691 {'added': [
690 692 FileNode(filename, content=content),
691 693 ]},
692 694 ]
693 695 self._add_commits_to_repo(self.repo.scm_instance(), commits)
694 696
695 697 def enable_downloads(self):
696 698 repo = self.repo
697 699 repo.enable_downloads = True
698 700 Session().add(repo)
699 701 Session().commit()
700 702
701 703 def cleanup(self):
702 704 for repo_name in reversed(self._cleanup_repos):
703 705 self._fixture.destroy_repo(repo_name)
704 706
705 707 def _add_commits_to_repo(self, repo, commits):
706 708 commit_ids = _add_commits_to_repo(repo, commits)
707 709 if not commit_ids:
708 710 return
709 711 self._commit_ids = commit_ids
710 712
711 713 # Creating refs for Git to allow fetching them from remote repository
712 714 if self.alias == 'git':
713 715 refs = {}
714 716 for message in self._commit_ids:
715 717 # TODO: mikhail: do more special chars replacements
716 718 ref_name = 'refs/test-refs/{}'.format(
717 719 message.replace(' ', ''))
718 720 refs[ref_name] = self._commit_ids[message]
719 721 self._create_refs(repo, refs)
720 722
721 723 def _create_refs(self, repo, refs):
722 724 for ref_name in refs:
723 725 repo.set_refs(ref_name, refs[ref_name])
724 726
725 727
726 728 def vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo):
727 729 if backend_alias not in request.config.getoption('--backends'):
728 730 pytest.skip("Backend %s not selected." % (backend_alias, ))
729 731
730 732 utils.check_xfail_backends(request.node, backend_alias)
731 733 utils.check_skip_backends(request.node, backend_alias)
732 734
733 735 repo_name = 'vcs_test_%s' % (backend_alias, )
734 736 repo_path = os.path.join(tests_tmp_path, repo_name)
735 737 backend = VcsBackend(
736 738 alias=backend_alias,
737 739 repo_path=repo_path,
738 740 test_name=request.node.name,
739 741 test_repo_container=test_repo)
740 742 request.addfinalizer(backend.cleanup)
741 743 return backend
742 744
743 745
744 746 @pytest.fixture()
745 747 def vcsbackend(request, backend_alias, tests_tmp_path, baseapp, test_repo):
746 748 """
747 749 Parametrized fixture which represents a single vcs backend implementation.
748 750
749 751 See the fixture `backend` for more details. This one implements the same
750 752 concept, but on vcs level. So it does not provide model instances etc.
751 753
752 754 Parameters are generated dynamically, see :func:`pytest_generate_tests`
753 755 for how this works.
754 756 """
755 757 return vcsbackend_base(request, backend_alias, tests_tmp_path, baseapp, test_repo)
756 758
757 759
758 760 @pytest.fixture()
759 761 def vcsbackend_git(request, tests_tmp_path, baseapp, test_repo):
760 762 return vcsbackend_base(request, 'git', tests_tmp_path, baseapp, test_repo)
761 763
762 764
763 765 @pytest.fixture()
764 766 def vcsbackend_hg(request, tests_tmp_path, baseapp, test_repo):
765 767 return vcsbackend_base(request, 'hg', tests_tmp_path, baseapp, test_repo)
766 768
767 769
768 770 @pytest.fixture()
769 771 def vcsbackend_svn(request, tests_tmp_path, baseapp, test_repo):
770 772 return vcsbackend_base(request, 'svn', tests_tmp_path, baseapp, test_repo)
771 773
772 774
773 775 @pytest.fixture()
774 776 def vcsbackend_stub(vcsbackend_git):
775 777 """
776 778 Use this to express that your test just needs a stub of a vcsbackend.
777 779
778 780 Plan is to eventually implement an in-memory stub to speed tests up.
779 781 """
780 782 return vcsbackend_git
781 783
782 784
783 785 class VcsBackend(object):
784 786 """
785 787 Represents the test configuration for one supported vcs backend.
786 788 """
787 789
788 790 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
789 791
790 792 def __init__(self, alias, repo_path, test_name, test_repo_container):
791 793 self.alias = alias
792 794 self._repo_path = repo_path
793 795 self._cleanup_repos = []
794 796 self._test_name = test_name
795 797 self._test_repo_container = test_repo_container
796 798
797 799 def __getitem__(self, key):
798 800 return self._test_repo_container(key, self.alias).scm_instance()
799 801
800 802 @property
801 803 def repo(self):
802 804 """
803 805 Returns the "current" repository. This is the vcs_test repo of the last
804 806 repo which has been created.
805 807 """
806 808 Repository = get_backend(self.alias)
807 809 return Repository(self._repo_path)
808 810
809 811 @property
810 812 def backend(self):
811 813 """
812 814 Returns the backend implementation class.
813 815 """
814 816 return get_backend(self.alias)
815 817
816 818 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None,
817 819 bare=False):
818 820 repo_name = self._next_repo_name()
819 821 self._repo_path = get_new_dir(repo_name)
820 822 repo_class = get_backend(self.alias)
821 823 src_url = None
822 824 if _clone_repo:
823 825 src_url = _clone_repo.path
824 826 repo = repo_class(self._repo_path, create=True, src_url=src_url, bare=bare)
825 827 self._cleanup_repos.append(repo)
826 828
827 829 commits = commits or [
828 830 {'message': 'Commit %s of %s' % (x, repo_name)}
829 831 for x in xrange(number_of_commits)]
830 832 _add_commits_to_repo(repo, commits)
831 833 return repo
832 834
833 835 def clone_repo(self, repo):
834 836 return self.create_repo(_clone_repo=repo)
835 837
836 838 def cleanup(self):
837 839 for repo in self._cleanup_repos:
838 840 shutil.rmtree(repo.path)
839 841
840 842 def new_repo_path(self):
841 843 repo_name = self._next_repo_name()
842 844 self._repo_path = get_new_dir(repo_name)
843 845 return self._repo_path
844 846
845 847 def _next_repo_name(self):
846 848 return "%s_%s" % (
847 849 self.invalid_repo_name.sub('_', self._test_name),
848 850 len(self._cleanup_repos))
849 851
850 852 def add_file(self, repo, filename, content='Test content\n'):
851 853 imc = repo.in_memory_commit
852 854 imc.add(FileNode(filename, content=content))
853 855 imc.commit(
854 856 message=u'Automatic commit from vcsbackend fixture',
855 857 author=u'Automatic <automatic@rhodecode.com>')
856 858
857 859 def ensure_file(self, filename, content='Test content\n'):
858 860 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
859 861 self.add_file(self.repo, filename, content)
860 862
861 863
862 864 def _add_commits_to_repo(vcs_repo, commits):
863 865 commit_ids = {}
864 866 if not commits:
865 867 return commit_ids
866 868
867 869 imc = vcs_repo.in_memory_commit
868 870 commit = None
869 871
870 872 for idx, commit in enumerate(commits):
871 873 message = unicode(commit.get('message', 'Commit %s' % idx))
872 874
873 875 for node in commit.get('added', []):
874 876 imc.add(FileNode(node.path, content=node.content))
875 877 for node in commit.get('changed', []):
876 878 imc.change(FileNode(node.path, content=node.content))
877 879 for node in commit.get('removed', []):
878 880 imc.remove(FileNode(node.path))
879 881
880 882 parents = [
881 883 vcs_repo.get_commit(commit_id=commit_ids[p])
882 884 for p in commit.get('parents', [])]
883 885
884 886 operations = ('added', 'changed', 'removed')
885 887 if not any((commit.get(o) for o in operations)):
886 888 imc.add(FileNode('file_%s' % idx, content=message))
887 889
888 890 commit = imc.commit(
889 891 message=message,
890 892 author=unicode(commit.get('author', 'Automatic <automatic@rhodecode.com>')),
891 893 date=commit.get('date'),
892 894 branch=commit.get('branch'),
893 895 parents=parents)
894 896
895 897 commit_ids[commit.message] = commit.raw_id
896 898
897 899 return commit_ids
898 900
899 901
900 902 @pytest.fixture()
901 903 def reposerver(request):
902 904 """
903 905 Allows to serve a backend repository
904 906 """
905 907
906 908 repo_server = RepoServer()
907 909 request.addfinalizer(repo_server.cleanup)
908 910 return repo_server
909 911
910 912
911 913 class RepoServer(object):
912 914 """
913 915 Utility to serve a local repository for the duration of a test case.
914 916
915 917 Supports only Subversion so far.
916 918 """
917 919
918 920 url = None
919 921
920 922 def __init__(self):
921 923 self._cleanup_servers = []
922 924
923 925 def serve(self, vcsrepo):
924 926 if vcsrepo.alias != 'svn':
925 927 raise TypeError("Backend %s not supported" % vcsrepo.alias)
926 928
927 929 proc = subprocess32.Popen(
928 930 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
929 931 '--root', vcsrepo.path])
930 932 self._cleanup_servers.append(proc)
931 933 self.url = 'svn://localhost'
932 934
933 935 def cleanup(self):
934 936 for proc in self._cleanup_servers:
935 937 proc.terminate()
936 938
937 939
938 940 @pytest.fixture()
939 941 def pr_util(backend, request, config_stub):
940 942 """
941 943 Utility for tests of models and for functional tests around pull requests.
942 944
943 945 It gives an instance of :class:`PRTestUtility` which provides various
944 946 utility methods around one pull request.
945 947
946 948 This fixture uses `backend` and inherits its parameterization.
947 949 """
948 950
949 951 util = PRTestUtility(backend)
950 952 request.addfinalizer(util.cleanup)
951 953
952 954 return util
953 955
954 956
955 957 class PRTestUtility(object):
956 958
957 959 pull_request = None
958 960 pull_request_id = None
959 961 mergeable_patcher = None
960 962 mergeable_mock = None
961 963 notification_patcher = None
962 964
963 965 def __init__(self, backend):
964 966 self.backend = backend
965 967
966 968 def create_pull_request(
967 969 self, commits=None, target_head=None, source_head=None,
968 970 revisions=None, approved=False, author=None, mergeable=False,
969 971 enable_notifications=True, name_suffix=u'', reviewers=None,
970 972 title=u"Test", description=u"Description"):
971 973 self.set_mergeable(mergeable)
972 974 if not enable_notifications:
973 975 # mock notification side effect
974 976 self.notification_patcher = mock.patch(
975 977 'rhodecode.model.notification.NotificationModel.create')
976 978 self.notification_patcher.start()
977 979
978 980 if not self.pull_request:
979 981 if not commits:
980 982 commits = [
981 983 {'message': 'c1'},
982 984 {'message': 'c2'},
983 985 {'message': 'c3'},
984 986 ]
985 987 target_head = 'c1'
986 988 source_head = 'c2'
987 989 revisions = ['c2']
988 990
989 991 self.commit_ids = self.backend.create_master_repo(commits)
990 992 self.target_repository = self.backend.create_repo(
991 993 heads=[target_head], name_suffix=name_suffix)
992 994 self.source_repository = self.backend.create_repo(
993 995 heads=[source_head], name_suffix=name_suffix)
994 996 self.author = author or UserModel().get_by_username(
995 997 TEST_USER_ADMIN_LOGIN)
996 998
997 999 model = PullRequestModel()
998 1000 self.create_parameters = {
999 1001 'created_by': self.author,
1000 1002 'source_repo': self.source_repository.repo_name,
1001 1003 'source_ref': self._default_branch_reference(source_head),
1002 1004 'target_repo': self.target_repository.repo_name,
1003 1005 'target_ref': self._default_branch_reference(target_head),
1004 1006 'revisions': [self.commit_ids[r] for r in revisions],
1005 1007 'reviewers': reviewers or self._get_reviewers(),
1006 1008 'title': title,
1007 1009 'description': description,
1008 1010 }
1009 1011 self.pull_request = model.create(**self.create_parameters)
1010 1012 assert model.get_versions(self.pull_request) == []
1011 1013
1012 1014 self.pull_request_id = self.pull_request.pull_request_id
1013 1015
1014 1016 if approved:
1015 1017 self.approve()
1016 1018
1017 1019 Session().add(self.pull_request)
1018 1020 Session().commit()
1019 1021
1020 1022 return self.pull_request
1021 1023
1022 1024 def approve(self):
1023 1025 self.create_status_votes(
1024 1026 ChangesetStatus.STATUS_APPROVED,
1025 1027 *self.pull_request.reviewers)
1026 1028
1027 1029 def close(self):
1028 1030 PullRequestModel().close_pull_request(self.pull_request, self.author)
1029 1031
1030 1032 def _default_branch_reference(self, commit_message):
1031 1033 reference = '%s:%s:%s' % (
1032 1034 'branch',
1033 1035 self.backend.default_branch_name,
1034 1036 self.commit_ids[commit_message])
1035 1037 return reference
1036 1038
1037 1039 def _get_reviewers(self):
1038 1040 return [
1039 1041 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
1040 1042 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1041 1043 ]
1042 1044
1043 1045 def update_source_repository(self, head=None):
1044 1046 heads = [head or 'c3']
1045 1047 self.backend.pull_heads(self.source_repository, heads=heads)
1046 1048
1047 1049 def add_one_commit(self, head=None):
1048 1050 self.update_source_repository(head=head)
1049 1051 old_commit_ids = set(self.pull_request.revisions)
1050 1052 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1051 1053 commit_ids = set(self.pull_request.revisions)
1052 1054 new_commit_ids = commit_ids - old_commit_ids
1053 1055 assert len(new_commit_ids) == 1
1054 1056 return new_commit_ids.pop()
1055 1057
1056 1058 def remove_one_commit(self):
1057 1059 assert len(self.pull_request.revisions) == 2
1058 1060 source_vcs = self.source_repository.scm_instance()
1059 1061 removed_commit_id = source_vcs.commit_ids[-1]
1060 1062
1061 1063 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1062 1064 # remove the if once that's sorted out.
1063 1065 if self.backend.alias == "git":
1064 1066 kwargs = {'branch_name': self.backend.default_branch_name}
1065 1067 else:
1066 1068 kwargs = {}
1067 1069 source_vcs.strip(removed_commit_id, **kwargs)
1068 1070
1069 1071 PullRequestModel().update_commits(self.pull_request, self.pull_request.author)
1070 1072 assert len(self.pull_request.revisions) == 1
1071 1073 return removed_commit_id
1072 1074
1073 1075 def create_comment(self, linked_to=None):
1074 1076 comment = CommentsModel().create(
1075 1077 text=u"Test comment",
1076 1078 repo=self.target_repository.repo_name,
1077 1079 user=self.author,
1078 1080 pull_request=self.pull_request)
1079 1081 assert comment.pull_request_version_id is None
1080 1082
1081 1083 if linked_to:
1082 1084 PullRequestModel()._link_comments_to_version(linked_to)
1083 1085
1084 1086 return comment
1085 1087
1086 1088 def create_inline_comment(
1087 1089 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1088 1090 comment = CommentsModel().create(
1089 1091 text=u"Test comment",
1090 1092 repo=self.target_repository.repo_name,
1091 1093 user=self.author,
1092 1094 line_no=line_no,
1093 1095 f_path=file_path,
1094 1096 pull_request=self.pull_request)
1095 1097 assert comment.pull_request_version_id is None
1096 1098
1097 1099 if linked_to:
1098 1100 PullRequestModel()._link_comments_to_version(linked_to)
1099 1101
1100 1102 return comment
1101 1103
1102 1104 def create_version_of_pull_request(self):
1103 1105 pull_request = self.create_pull_request()
1104 1106 version = PullRequestModel()._create_version_from_snapshot(
1105 1107 pull_request)
1106 1108 return version
1107 1109
1108 1110 def create_status_votes(self, status, *reviewers):
1109 1111 for reviewer in reviewers:
1110 1112 ChangesetStatusModel().set_status(
1111 1113 repo=self.pull_request.target_repo,
1112 1114 status=status,
1113 1115 user=reviewer.user_id,
1114 1116 pull_request=self.pull_request)
1115 1117
1116 1118 def set_mergeable(self, value):
1117 1119 if not self.mergeable_patcher:
1118 1120 self.mergeable_patcher = mock.patch.object(
1119 1121 VcsSettingsModel, 'get_general_settings')
1120 1122 self.mergeable_mock = self.mergeable_patcher.start()
1121 1123 self.mergeable_mock.return_value = {
1122 1124 'rhodecode_pr_merge_enabled': value}
1123 1125
1124 1126 def cleanup(self):
1125 1127 # In case the source repository is already cleaned up, the pull
1126 1128 # request will already be deleted.
1127 1129 pull_request = PullRequest().get(self.pull_request_id)
1128 1130 if pull_request:
1129 1131 PullRequestModel().delete(pull_request, pull_request.author)
1130 1132 Session().commit()
1131 1133
1132 1134 if self.notification_patcher:
1133 1135 self.notification_patcher.stop()
1134 1136
1135 1137 if self.mergeable_patcher:
1136 1138 self.mergeable_patcher.stop()
1137 1139
1138 1140
1139 1141 @pytest.fixture()
1140 1142 def user_admin(baseapp):
1141 1143 """
1142 1144 Provides the default admin test user as an instance of `db.User`.
1143 1145 """
1144 1146 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1145 1147 return user
1146 1148
1147 1149
1148 1150 @pytest.fixture()
1149 1151 def user_regular(baseapp):
1150 1152 """
1151 1153 Provides the default regular test user as an instance of `db.User`.
1152 1154 """
1153 1155 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1154 1156 return user
1155 1157
1156 1158
1157 1159 @pytest.fixture()
1158 1160 def user_util(request, db_connection):
1159 1161 """
1160 1162 Provides a wired instance of `UserUtility` with integrated cleanup.
1161 1163 """
1162 1164 utility = UserUtility(test_name=request.node.name)
1163 1165 request.addfinalizer(utility.cleanup)
1164 1166 return utility
1165 1167
1166 1168
1167 1169 # TODO: johbo: Split this up into utilities per domain or something similar
1168 1170 class UserUtility(object):
1169 1171
1170 1172 def __init__(self, test_name="test"):
1171 1173 self._test_name = self._sanitize_name(test_name)
1172 1174 self.fixture = Fixture()
1173 1175 self.repo_group_ids = []
1174 1176 self.repos_ids = []
1175 1177 self.user_ids = []
1176 1178 self.user_group_ids = []
1177 1179 self.user_repo_permission_ids = []
1178 1180 self.user_group_repo_permission_ids = []
1179 1181 self.user_repo_group_permission_ids = []
1180 1182 self.user_group_repo_group_permission_ids = []
1181 1183 self.user_user_group_permission_ids = []
1182 1184 self.user_group_user_group_permission_ids = []
1183 1185 self.user_permissions = []
1184 1186
1185 1187 def _sanitize_name(self, name):
1186 1188 for char in ['[', ']']:
1187 1189 name = name.replace(char, '_')
1188 1190 return name
1189 1191
1190 1192 def create_repo_group(
1191 1193 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1192 1194 group_name = "{prefix}_repogroup_{count}".format(
1193 1195 prefix=self._test_name,
1194 1196 count=len(self.repo_group_ids))
1195 1197 repo_group = self.fixture.create_repo_group(
1196 1198 group_name, cur_user=owner)
1197 1199 if auto_cleanup:
1198 1200 self.repo_group_ids.append(repo_group.group_id)
1199 1201 return repo_group
1200 1202
1201 1203 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1202 1204 auto_cleanup=True, repo_type='hg', bare=False):
1203 1205 repo_name = "{prefix}_repository_{count}".format(
1204 1206 prefix=self._test_name,
1205 1207 count=len(self.repos_ids))
1206 1208
1207 1209 repository = self.fixture.create_repo(
1208 1210 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type, bare=bare)
1209 1211 if auto_cleanup:
1210 1212 self.repos_ids.append(repository.repo_id)
1211 1213 return repository
1212 1214
1213 1215 def create_user(self, auto_cleanup=True, **kwargs):
1214 1216 user_name = "{prefix}_user_{count}".format(
1215 1217 prefix=self._test_name,
1216 1218 count=len(self.user_ids))
1217 1219 user = self.fixture.create_user(user_name, **kwargs)
1218 1220 if auto_cleanup:
1219 1221 self.user_ids.append(user.user_id)
1220 1222 return user
1221 1223
1222 1224 def create_additional_user_email(self, user, email):
1223 1225 uem = self.fixture.create_additional_user_email(user=user, email=email)
1224 1226 return uem
1225 1227
1226 1228 def create_user_with_group(self):
1227 1229 user = self.create_user()
1228 1230 user_group = self.create_user_group(members=[user])
1229 1231 return user, user_group
1230 1232
1231 1233 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1232 1234 auto_cleanup=True, **kwargs):
1233 1235 group_name = "{prefix}_usergroup_{count}".format(
1234 1236 prefix=self._test_name,
1235 1237 count=len(self.user_group_ids))
1236 1238 user_group = self.fixture.create_user_group(
1237 1239 group_name, cur_user=owner, **kwargs)
1238 1240
1239 1241 if auto_cleanup:
1240 1242 self.user_group_ids.append(user_group.users_group_id)
1241 1243 if members:
1242 1244 for user in members:
1243 1245 UserGroupModel().add_user_to_group(user_group, user)
1244 1246 return user_group
1245 1247
1246 1248 def grant_user_permission(self, user_name, permission_name):
1247 1249 self.inherit_default_user_permissions(user_name, False)
1248 1250 self.user_permissions.append((user_name, permission_name))
1249 1251
1250 1252 def grant_user_permission_to_repo_group(
1251 1253 self, repo_group, user, permission_name):
1252 1254 permission = RepoGroupModel().grant_user_permission(
1253 1255 repo_group, user, permission_name)
1254 1256 self.user_repo_group_permission_ids.append(
1255 1257 (repo_group.group_id, user.user_id))
1256 1258 return permission
1257 1259
1258 1260 def grant_user_group_permission_to_repo_group(
1259 1261 self, repo_group, user_group, permission_name):
1260 1262 permission = RepoGroupModel().grant_user_group_permission(
1261 1263 repo_group, user_group, permission_name)
1262 1264 self.user_group_repo_group_permission_ids.append(
1263 1265 (repo_group.group_id, user_group.users_group_id))
1264 1266 return permission
1265 1267
1266 1268 def grant_user_permission_to_repo(
1267 1269 self, repo, user, permission_name):
1268 1270 permission = RepoModel().grant_user_permission(
1269 1271 repo, user, permission_name)
1270 1272 self.user_repo_permission_ids.append(
1271 1273 (repo.repo_id, user.user_id))
1272 1274 return permission
1273 1275
1274 1276 def grant_user_group_permission_to_repo(
1275 1277 self, repo, user_group, permission_name):
1276 1278 permission = RepoModel().grant_user_group_permission(
1277 1279 repo, user_group, permission_name)
1278 1280 self.user_group_repo_permission_ids.append(
1279 1281 (repo.repo_id, user_group.users_group_id))
1280 1282 return permission
1281 1283
1282 1284 def grant_user_permission_to_user_group(
1283 1285 self, target_user_group, user, permission_name):
1284 1286 permission = UserGroupModel().grant_user_permission(
1285 1287 target_user_group, user, permission_name)
1286 1288 self.user_user_group_permission_ids.append(
1287 1289 (target_user_group.users_group_id, user.user_id))
1288 1290 return permission
1289 1291
1290 1292 def grant_user_group_permission_to_user_group(
1291 1293 self, target_user_group, user_group, permission_name):
1292 1294 permission = UserGroupModel().grant_user_group_permission(
1293 1295 target_user_group, user_group, permission_name)
1294 1296 self.user_group_user_group_permission_ids.append(
1295 1297 (target_user_group.users_group_id, user_group.users_group_id))
1296 1298 return permission
1297 1299
1298 1300 def revoke_user_permission(self, user_name, permission_name):
1299 1301 self.inherit_default_user_permissions(user_name, True)
1300 1302 UserModel().revoke_perm(user_name, permission_name)
1301 1303
1302 1304 def inherit_default_user_permissions(self, user_name, value):
1303 1305 user = UserModel().get_by_username(user_name)
1304 1306 user.inherit_default_permissions = value
1305 1307 Session().add(user)
1306 1308 Session().commit()
1307 1309
1308 1310 def cleanup(self):
1309 1311 self._cleanup_permissions()
1310 1312 self._cleanup_repos()
1311 1313 self._cleanup_repo_groups()
1312 1314 self._cleanup_user_groups()
1313 1315 self._cleanup_users()
1314 1316
1315 1317 def _cleanup_permissions(self):
1316 1318 if self.user_permissions:
1317 1319 for user_name, permission_name in self.user_permissions:
1318 1320 self.revoke_user_permission(user_name, permission_name)
1319 1321
1320 1322 for permission in self.user_repo_permission_ids:
1321 1323 RepoModel().revoke_user_permission(*permission)
1322 1324
1323 1325 for permission in self.user_group_repo_permission_ids:
1324 1326 RepoModel().revoke_user_group_permission(*permission)
1325 1327
1326 1328 for permission in self.user_repo_group_permission_ids:
1327 1329 RepoGroupModel().revoke_user_permission(*permission)
1328 1330
1329 1331 for permission in self.user_group_repo_group_permission_ids:
1330 1332 RepoGroupModel().revoke_user_group_permission(*permission)
1331 1333
1332 1334 for permission in self.user_user_group_permission_ids:
1333 1335 UserGroupModel().revoke_user_permission(*permission)
1334 1336
1335 1337 for permission in self.user_group_user_group_permission_ids:
1336 1338 UserGroupModel().revoke_user_group_permission(*permission)
1337 1339
1338 1340 def _cleanup_repo_groups(self):
1339 1341 def _repo_group_compare(first_group_id, second_group_id):
1340 1342 """
1341 1343 Gives higher priority to the groups with the most complex paths
1342 1344 """
1343 1345 first_group = RepoGroup.get(first_group_id)
1344 1346 second_group = RepoGroup.get(second_group_id)
1345 1347 first_group_parts = (
1346 1348 len(first_group.group_name.split('/')) if first_group else 0)
1347 1349 second_group_parts = (
1348 1350 len(second_group.group_name.split('/')) if second_group else 0)
1349 1351 return cmp(second_group_parts, first_group_parts)
1350 1352
1351 1353 sorted_repo_group_ids = sorted(
1352 1354 self.repo_group_ids, cmp=_repo_group_compare)
1353 1355 for repo_group_id in sorted_repo_group_ids:
1354 1356 self.fixture.destroy_repo_group(repo_group_id)
1355 1357
1356 1358 def _cleanup_repos(self):
1357 1359 sorted_repos_ids = sorted(self.repos_ids)
1358 1360 for repo_id in sorted_repos_ids:
1359 1361 self.fixture.destroy_repo(repo_id)
1360 1362
1361 1363 def _cleanup_user_groups(self):
1362 1364 def _user_group_compare(first_group_id, second_group_id):
1363 1365 """
1364 1366 Gives higher priority to the groups with the most complex paths
1365 1367 """
1366 1368 first_group = UserGroup.get(first_group_id)
1367 1369 second_group = UserGroup.get(second_group_id)
1368 1370 first_group_parts = (
1369 1371 len(first_group.users_group_name.split('/'))
1370 1372 if first_group else 0)
1371 1373 second_group_parts = (
1372 1374 len(second_group.users_group_name.split('/'))
1373 1375 if second_group else 0)
1374 1376 return cmp(second_group_parts, first_group_parts)
1375 1377
1376 1378 sorted_user_group_ids = sorted(
1377 1379 self.user_group_ids, cmp=_user_group_compare)
1378 1380 for user_group_id in sorted_user_group_ids:
1379 1381 self.fixture.destroy_user_group(user_group_id)
1380 1382
1381 1383 def _cleanup_users(self):
1382 1384 for user_id in self.user_ids:
1383 1385 self.fixture.destroy_user(user_id)
1384 1386
1385 1387
1386 1388 # TODO: Think about moving this into a pytest-pyro package and make it a
1387 1389 # pytest plugin
1388 1390 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1389 1391 def pytest_runtest_makereport(item, call):
1390 1392 """
1391 1393 Adding the remote traceback if the exception has this information.
1392 1394
1393 1395 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1394 1396 to the exception instance.
1395 1397 """
1396 1398 outcome = yield
1397 1399 report = outcome.get_result()
1398 1400 if call.excinfo:
1399 1401 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1400 1402
1401 1403
1402 1404 def _add_vcsserver_remote_traceback(report, exc):
1403 1405 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1404 1406
1405 1407 if vcsserver_traceback:
1406 1408 section = 'VCSServer remote traceback ' + report.when
1407 1409 report.sections.append((section, vcsserver_traceback))
1408 1410
1409 1411
1410 1412 @pytest.fixture(scope='session')
1411 1413 def testrun():
1412 1414 return {
1413 1415 'uuid': uuid.uuid4(),
1414 1416 'start': datetime.datetime.utcnow().isoformat(),
1415 1417 'timestamp': int(time.time()),
1416 1418 }
1417 1419
1418 1420
1419 1421 class AppenlightClient(object):
1420 1422
1421 1423 url_template = '{url}?protocol_version=0.5'
1422 1424
1423 1425 def __init__(
1424 1426 self, url, api_key, add_server=True, add_timestamp=True,
1425 1427 namespace=None, request=None, testrun=None):
1426 1428 self.url = self.url_template.format(url=url)
1427 1429 self.api_key = api_key
1428 1430 self.add_server = add_server
1429 1431 self.add_timestamp = add_timestamp
1430 1432 self.namespace = namespace
1431 1433 self.request = request
1432 1434 self.server = socket.getfqdn(socket.gethostname())
1433 1435 self.tags_before = {}
1434 1436 self.tags_after = {}
1435 1437 self.stats = []
1436 1438 self.testrun = testrun or {}
1437 1439
1438 1440 def tag_before(self, tag, value):
1439 1441 self.tags_before[tag] = value
1440 1442
1441 1443 def tag_after(self, tag, value):
1442 1444 self.tags_after[tag] = value
1443 1445
1444 1446 def collect(self, data):
1445 1447 if self.add_server:
1446 1448 data.setdefault('server', self.server)
1447 1449 if self.add_timestamp:
1448 1450 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1449 1451 if self.namespace:
1450 1452 data.setdefault('namespace', self.namespace)
1451 1453 if self.request:
1452 1454 data.setdefault('request', self.request)
1453 1455 self.stats.append(data)
1454 1456
1455 1457 def send_stats(self):
1456 1458 tags = [
1457 1459 ('testrun', self.request),
1458 1460 ('testrun.start', self.testrun['start']),
1459 1461 ('testrun.timestamp', self.testrun['timestamp']),
1460 1462 ('test', self.namespace),
1461 1463 ]
1462 1464 for key, value in self.tags_before.items():
1463 1465 tags.append((key + '.before', value))
1464 1466 try:
1465 1467 delta = self.tags_after[key] - value
1466 1468 tags.append((key + '.delta', delta))
1467 1469 except Exception:
1468 1470 pass
1469 1471 for key, value in self.tags_after.items():
1470 1472 tags.append((key + '.after', value))
1471 1473 self.collect({
1472 1474 'message': "Collected tags",
1473 1475 'tags': tags,
1474 1476 })
1475 1477
1476 1478 response = requests.post(
1477 1479 self.url,
1478 1480 headers={
1479 1481 'X-appenlight-api-key': self.api_key},
1480 1482 json=self.stats,
1481 1483 )
1482 1484
1483 1485 if not response.status_code == 200:
1484 1486 pprint.pprint(self.stats)
1485 1487 print(response.headers)
1486 1488 print(response.text)
1487 1489 raise Exception('Sending to appenlight failed')
1488 1490
1489 1491
1490 1492 @pytest.fixture()
1491 1493 def gist_util(request, db_connection):
1492 1494 """
1493 1495 Provides a wired instance of `GistUtility` with integrated cleanup.
1494 1496 """
1495 1497 utility = GistUtility()
1496 1498 request.addfinalizer(utility.cleanup)
1497 1499 return utility
1498 1500
1499 1501
1500 1502 class GistUtility(object):
1501 1503 def __init__(self):
1502 1504 self.fixture = Fixture()
1503 1505 self.gist_ids = []
1504 1506
1505 1507 def create_gist(self, **kwargs):
1506 1508 gist = self.fixture.create_gist(**kwargs)
1507 1509 self.gist_ids.append(gist.gist_id)
1508 1510 return gist
1509 1511
1510 1512 def cleanup(self):
1511 1513 for id_ in self.gist_ids:
1512 1514 self.fixture.destroy_gists(str(id_))
1513 1515
1514 1516
1515 1517 @pytest.fixture()
1516 1518 def enabled_backends(request):
1517 1519 backends = request.config.option.backends
1518 1520 return backends[:]
1519 1521
1520 1522
1521 1523 @pytest.fixture()
1522 1524 def settings_util(request, db_connection):
1523 1525 """
1524 1526 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1525 1527 """
1526 1528 utility = SettingsUtility()
1527 1529 request.addfinalizer(utility.cleanup)
1528 1530 return utility
1529 1531
1530 1532
1531 1533 class SettingsUtility(object):
1532 1534 def __init__(self):
1533 1535 self.rhodecode_ui_ids = []
1534 1536 self.rhodecode_setting_ids = []
1535 1537 self.repo_rhodecode_ui_ids = []
1536 1538 self.repo_rhodecode_setting_ids = []
1537 1539
1538 1540 def create_repo_rhodecode_ui(
1539 1541 self, repo, section, value, key=None, active=True, cleanup=True):
1540 1542 key = key or hashlib.sha1(
1541 1543 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1542 1544
1543 1545 setting = RepoRhodeCodeUi()
1544 1546 setting.repository_id = repo.repo_id
1545 1547 setting.ui_section = section
1546 1548 setting.ui_value = value
1547 1549 setting.ui_key = key
1548 1550 setting.ui_active = active
1549 1551 Session().add(setting)
1550 1552 Session().commit()
1551 1553
1552 1554 if cleanup:
1553 1555 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1554 1556 return setting
1555 1557
1556 1558 def create_rhodecode_ui(
1557 1559 self, section, value, key=None, active=True, cleanup=True):
1558 1560 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1559 1561
1560 1562 setting = RhodeCodeUi()
1561 1563 setting.ui_section = section
1562 1564 setting.ui_value = value
1563 1565 setting.ui_key = key
1564 1566 setting.ui_active = active
1565 1567 Session().add(setting)
1566 1568 Session().commit()
1567 1569
1568 1570 if cleanup:
1569 1571 self.rhodecode_ui_ids.append(setting.ui_id)
1570 1572 return setting
1571 1573
1572 1574 def create_repo_rhodecode_setting(
1573 1575 self, repo, name, value, type_, cleanup=True):
1574 1576 setting = RepoRhodeCodeSetting(
1575 1577 repo.repo_id, key=name, val=value, type=type_)
1576 1578 Session().add(setting)
1577 1579 Session().commit()
1578 1580
1579 1581 if cleanup:
1580 1582 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1581 1583 return setting
1582 1584
1583 1585 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1584 1586 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1585 1587 Session().add(setting)
1586 1588 Session().commit()
1587 1589
1588 1590 if cleanup:
1589 1591 self.rhodecode_setting_ids.append(setting.app_settings_id)
1590 1592
1591 1593 return setting
1592 1594
1593 1595 def cleanup(self):
1594 1596 for id_ in self.rhodecode_ui_ids:
1595 1597 setting = RhodeCodeUi.get(id_)
1596 1598 Session().delete(setting)
1597 1599
1598 1600 for id_ in self.rhodecode_setting_ids:
1599 1601 setting = RhodeCodeSetting.get(id_)
1600 1602 Session().delete(setting)
1601 1603
1602 1604 for id_ in self.repo_rhodecode_ui_ids:
1603 1605 setting = RepoRhodeCodeUi.get(id_)
1604 1606 Session().delete(setting)
1605 1607
1606 1608 for id_ in self.repo_rhodecode_setting_ids:
1607 1609 setting = RepoRhodeCodeSetting.get(id_)
1608 1610 Session().delete(setting)
1609 1611
1610 1612 Session().commit()
1611 1613
1612 1614
1613 1615 @pytest.fixture()
1614 1616 def no_notifications(request):
1615 1617 notification_patcher = mock.patch(
1616 1618 'rhodecode.model.notification.NotificationModel.create')
1617 1619 notification_patcher.start()
1618 1620 request.addfinalizer(notification_patcher.stop)
1619 1621
1620 1622
1621 1623 @pytest.fixture(scope='session')
1622 1624 def repeat(request):
1623 1625 """
1624 1626 The number of repetitions is based on this fixture.
1625 1627
1626 1628 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1627 1629 tests are not too slow in our default test suite.
1628 1630 """
1629 1631 return request.config.getoption('--repeat')
1630 1632
1631 1633
1632 1634 @pytest.fixture()
1633 1635 def rhodecode_fixtures():
1634 1636 return Fixture()
1635 1637
1636 1638
1637 1639 @pytest.fixture()
1638 1640 def context_stub():
1639 1641 """
1640 1642 Stub context object.
1641 1643 """
1642 1644 context = pyramid.testing.DummyResource()
1643 1645 return context
1644 1646
1645 1647
1646 1648 @pytest.fixture()
1647 1649 def request_stub():
1648 1650 """
1649 1651 Stub request object.
1650 1652 """
1651 1653 from rhodecode.lib.base import bootstrap_request
1652 1654 request = bootstrap_request(scheme='https')
1653 1655 return request
1654 1656
1655 1657
1656 1658 @pytest.fixture()
1657 1659 def config_stub(request, request_stub):
1658 1660 """
1659 1661 Set up pyramid.testing and return the Configurator.
1660 1662 """
1661 1663 from rhodecode.lib.base import bootstrap_config
1662 1664 config = bootstrap_config(request=request_stub)
1663 1665
1664 1666 @request.addfinalizer
1665 1667 def cleanup():
1666 1668 pyramid.testing.tearDown()
1667 1669
1668 1670 return config
1669 1671
1670 1672
1671 1673 @pytest.fixture()
1672 1674 def StubIntegrationType():
1673 1675 class _StubIntegrationType(IntegrationTypeBase):
1674 1676 """ Test integration type class """
1675 1677
1676 1678 key = 'test'
1677 1679 display_name = 'Test integration type'
1678 1680 description = 'A test integration type for testing'
1679 1681
1680 1682 @classmethod
1681 1683 def icon(cls):
1682 1684 return 'test_icon_html_image'
1683 1685
1684 1686 def __init__(self, settings):
1685 1687 super(_StubIntegrationType, self).__init__(settings)
1686 1688 self.sent_events = [] # for testing
1687 1689
1688 1690 def send_event(self, event):
1689 1691 self.sent_events.append(event)
1690 1692
1691 1693 def settings_schema(self):
1692 1694 class SettingsSchema(colander.Schema):
1693 1695 test_string_field = colander.SchemaNode(
1694 1696 colander.String(),
1695 1697 missing=colander.required,
1696 1698 title='test string field',
1697 1699 )
1698 1700 test_int_field = colander.SchemaNode(
1699 1701 colander.Int(),
1700 1702 title='some integer setting',
1701 1703 )
1702 1704 return SettingsSchema()
1703 1705
1704 1706
1705 1707 integration_type_registry.register_integration_type(_StubIntegrationType)
1706 1708 return _StubIntegrationType
1707 1709
1708 1710 @pytest.fixture()
1709 1711 def stub_integration_settings():
1710 1712 return {
1711 1713 'test_string_field': 'some data',
1712 1714 'test_int_field': 100,
1713 1715 }
1714 1716
1715 1717
1716 1718 @pytest.fixture()
1717 1719 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1718 1720 stub_integration_settings):
1719 1721 integration = IntegrationModel().create(
1720 1722 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1721 1723 name='test repo integration',
1722 1724 repo=repo_stub, repo_group=None, child_repos_only=None)
1723 1725
1724 1726 @request.addfinalizer
1725 1727 def cleanup():
1726 1728 IntegrationModel().delete(integration)
1727 1729
1728 1730 return integration
1729 1731
1730 1732
1731 1733 @pytest.fixture()
1732 1734 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1733 1735 stub_integration_settings):
1734 1736 integration = IntegrationModel().create(
1735 1737 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1736 1738 name='test repogroup integration',
1737 1739 repo=None, repo_group=test_repo_group, child_repos_only=True)
1738 1740
1739 1741 @request.addfinalizer
1740 1742 def cleanup():
1741 1743 IntegrationModel().delete(integration)
1742 1744
1743 1745 return integration
1744 1746
1745 1747
1746 1748 @pytest.fixture()
1747 1749 def repogroup_recursive_integration_stub(request, test_repo_group,
1748 1750 StubIntegrationType, stub_integration_settings):
1749 1751 integration = IntegrationModel().create(
1750 1752 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1751 1753 name='test recursive repogroup integration',
1752 1754 repo=None, repo_group=test_repo_group, child_repos_only=False)
1753 1755
1754 1756 @request.addfinalizer
1755 1757 def cleanup():
1756 1758 IntegrationModel().delete(integration)
1757 1759
1758 1760 return integration
1759 1761
1760 1762
1761 1763 @pytest.fixture()
1762 1764 def global_integration_stub(request, StubIntegrationType,
1763 1765 stub_integration_settings):
1764 1766 integration = IntegrationModel().create(
1765 1767 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1766 1768 name='test global integration',
1767 1769 repo=None, repo_group=None, child_repos_only=None)
1768 1770
1769 1771 @request.addfinalizer
1770 1772 def cleanup():
1771 1773 IntegrationModel().delete(integration)
1772 1774
1773 1775 return integration
1774 1776
1775 1777
1776 1778 @pytest.fixture()
1777 1779 def root_repos_integration_stub(request, StubIntegrationType,
1778 1780 stub_integration_settings):
1779 1781 integration = IntegrationModel().create(
1780 1782 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1781 1783 name='test global integration',
1782 1784 repo=None, repo_group=None, child_repos_only=True)
1783 1785
1784 1786 @request.addfinalizer
1785 1787 def cleanup():
1786 1788 IntegrationModel().delete(integration)
1787 1789
1788 1790 return integration
1789 1791
1790 1792
1791 1793 @pytest.fixture()
1792 1794 def local_dt_to_utc():
1793 1795 def _factory(dt):
1794 1796 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1795 1797 dateutil.tz.tzutc()).replace(tzinfo=None)
1796 1798 return _factory
1797 1799
1798 1800
1799 1801 @pytest.fixture()
1800 1802 def disable_anonymous_user(request, baseapp):
1801 1803 set_anonymous_access(False)
1802 1804
1803 1805 @request.addfinalizer
1804 1806 def cleanup():
1805 1807 set_anonymous_access(True)
1806 1808
1807 1809
1808 1810 @pytest.fixture(scope='module')
1809 1811 def rc_fixture(request):
1810 1812 return Fixture()
1811 1813
1812 1814
1813 1815 @pytest.fixture()
1814 1816 def repo_groups(request):
1815 1817 fixture = Fixture()
1816 1818
1817 1819 session = Session()
1818 1820 zombie_group = fixture.create_repo_group('zombie')
1819 1821 parent_group = fixture.create_repo_group('parent')
1820 1822 child_group = fixture.create_repo_group('parent/child')
1821 1823 groups_in_db = session.query(RepoGroup).all()
1822 1824 assert len(groups_in_db) == 3
1823 1825 assert child_group.group_parent_id == parent_group.group_id
1824 1826
1825 1827 @request.addfinalizer
1826 1828 def cleanup():
1827 1829 fixture.destroy_repo_group(zombie_group)
1828 1830 fixture.destroy_repo_group(child_group)
1829 1831 fixture.destroy_repo_group(parent_group)
1830 1832
1831 1833 return zombie_group, parent_group, child_group
General Comments 0
You need to be logged in to leave comments. Login now