##// END OF EJS Templates
pull-requests: fixed a case when template marker was used in description field....
milka -
r4631:21211d2f stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1658 +1,1679 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 119 def test_show_versions_of_pr(self, backend, csrf_token):
120 120 commits = [
121 121 {'message': 'initial-commit',
122 122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123 123
124 124 {'message': 'commit-1',
125 125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 126 # Above is the initial version of PR that changes a single line
127 127
128 128 # from now on we'll add 3x commit adding a nother line on each step
129 129 {'message': 'commit-2',
130 130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131 131
132 132 {'message': 'commit-3',
133 133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134 134
135 135 {'message': 'commit-4',
136 136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 137 ]
138 138
139 139 commit_ids = backend.create_master_repo(commits)
140 140 target = backend.create_repo(heads=['initial-commit'])
141 141 source = backend.create_repo(heads=['commit-1'])
142 142 source_repo_name = source.repo_name
143 143 target_repo_name = target.repo_name
144 144
145 145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149 149
150 150 response = self.app.post(
151 151 route_path('pullrequest_create', repo_name=source.repo_name),
152 152 [
153 153 ('source_repo', source_repo_name),
154 154 ('source_ref', source_ref),
155 155 ('target_repo', target_repo_name),
156 156 ('target_ref', target_ref),
157 157 ('common_ancestor', commit_ids['initial-commit']),
158 158 ('pullrequest_title', 'Title'),
159 159 ('pullrequest_desc', 'Description'),
160 160 ('description_renderer', 'markdown'),
161 161 ('__start__', 'review_members:sequence'),
162 162 ('__start__', 'reviewer:mapping'),
163 163 ('user_id', '1'),
164 164 ('__start__', 'reasons:sequence'),
165 165 ('reason', 'Some reason'),
166 166 ('__end__', 'reasons:sequence'),
167 167 ('__start__', 'rules:sequence'),
168 168 ('__end__', 'rules:sequence'),
169 169 ('mandatory', 'False'),
170 170 ('__end__', 'reviewer:mapping'),
171 171 ('__end__', 'review_members:sequence'),
172 172 ('__start__', 'revisions:sequence'),
173 173 ('revisions', commit_ids['commit-1']),
174 174 ('__end__', 'revisions:sequence'),
175 175 ('user', ''),
176 176 ('csrf_token', csrf_token),
177 177 ],
178 178 status=302)
179 179
180 180 location = response.headers['Location']
181 181
182 182 pull_request_id = location.rsplit('/', 1)[1]
183 183 assert pull_request_id != 'new'
184 184 pull_request = PullRequest.get(int(pull_request_id))
185 185
186 186 pull_request_id = pull_request.pull_request_id
187 187
188 188 # Show initial version of PR
189 189 response = self.app.get(
190 190 route_path('pullrequest_show',
191 191 repo_name=target_repo_name,
192 192 pull_request_id=pull_request_id))
193 193
194 194 response.mustcontain('commit-1')
195 195 response.mustcontain(no=['commit-2'])
196 196 response.mustcontain(no=['commit-3'])
197 197 response.mustcontain(no=['commit-4'])
198 198
199 199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 200 response.mustcontain(no=['LINE3'])
201 201 response.mustcontain(no=['LINE4'])
202 202 response.mustcontain(no=['LINE5'])
203 203
204 204 # update PR #1
205 205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 206 backend.pull_heads(source_repo, heads=['commit-2'])
207 207 response = self.app.post(
208 208 route_path('pullrequest_update',
209 209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211 211
212 212 # update PR #2
213 213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 214 backend.pull_heads(source_repo, heads=['commit-3'])
215 215 response = self.app.post(
216 216 route_path('pullrequest_update',
217 217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219 219
220 220 # update PR #3
221 221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 222 backend.pull_heads(source_repo, heads=['commit-4'])
223 223 response = self.app.post(
224 224 route_path('pullrequest_update',
225 225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227 227
228 228 # Show final version !
229 229 response = self.app.get(
230 230 route_path('pullrequest_show',
231 231 repo_name=target_repo_name,
232 232 pull_request_id=pull_request_id))
233 233
234 234 # 3 updates, and the latest == 4
235 235 response.mustcontain('4 versions available for this pull request')
236 236 response.mustcontain(no=['rhodecode diff rendering error'])
237 237
238 238 # initial show must have 3 commits, and 3 adds
239 239 response.mustcontain('commit-1')
240 240 response.mustcontain('commit-2')
241 241 response.mustcontain('commit-3')
242 242 response.mustcontain('commit-4')
243 243
244 244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248 248
249 249 # fetch versions
250 250 pr = PullRequest.get(pull_request_id)
251 251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 252 assert len(versions) == 3
253 253
254 254 # show v1,v2,v3,v4
255 255 def cb_line(text):
256 256 return 'cb-addition"></span><span>{}</span>'.format(text)
257 257
258 258 def cb_context(text):
259 259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 260 '</span><span>{}</span></span>'.format(text)
261 261
262 262 commit_tests = {
263 263 # in response, not in response
264 264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 268 }
269 269 diff_tests = {
270 270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 274 }
275 275 for idx, ver in enumerate(versions, 1):
276 276
277 277 response = self.app.get(
278 278 route_path('pullrequest_show',
279 279 repo_name=target_repo_name,
280 280 pull_request_id=pull_request_id,
281 281 params={'version': ver}))
282 282
283 283 response.mustcontain(no=['rhodecode diff rendering error'])
284 284 response.mustcontain('Showing changes at v{}'.format(idx))
285 285
286 286 yes, no = commit_tests[idx]
287 287 for y in yes:
288 288 response.mustcontain(y)
289 289 for n in no:
290 290 response.mustcontain(no=n)
291 291
292 292 yes, no = diff_tests[idx]
293 293 for y in yes:
294 294 response.mustcontain(cb_line(y))
295 295 for n in no:
296 296 response.mustcontain(no=n)
297 297
298 298 # show diff between versions
299 299 diff_compare_tests = {
300 300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 303 }
304 304 for idx, ver in enumerate(versions, 1):
305 305 adds, context = diff_compare_tests[idx]
306 306
307 307 to_ver = ver+1
308 308 if idx == 3:
309 309 to_ver = 'latest'
310 310
311 311 response = self.app.get(
312 312 route_path('pullrequest_show',
313 313 repo_name=target_repo_name,
314 314 pull_request_id=pull_request_id,
315 315 params={'from_version': versions[0], 'version': to_ver}))
316 316
317 317 response.mustcontain(no=['rhodecode diff rendering error'])
318 318
319 319 for a in adds:
320 320 response.mustcontain(cb_line(a))
321 321 for c in context:
322 322 response.mustcontain(cb_context(c))
323 323
324 324 # test version v2 -> v3
325 325 response = self.app.get(
326 326 route_path('pullrequest_show',
327 327 repo_name=target_repo_name,
328 328 pull_request_id=pull_request_id,
329 329 params={'from_version': versions[1], 'version': versions[2]}))
330 330
331 331 response.mustcontain(cb_context('LINE1'))
332 332 response.mustcontain(cb_context('LINE2'))
333 333 response.mustcontain(cb_context('LINE3'))
334 334 response.mustcontain(cb_line('LINE4'))
335 335
336 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 337 # Logout
338 338 response = self.app.post(
339 339 h.route_path('logout'),
340 340 params={'csrf_token': csrf_token})
341 341 # Login as regular user
342 342 response = self.app.post(h.route_path('login'),
343 343 {'username': TEST_USER_REGULAR_LOGIN,
344 344 'password': 'test12'})
345 345
346 346 pull_request = pr_util.create_pull_request(
347 347 author=TEST_USER_REGULAR_LOGIN)
348 348
349 349 response = self.app.get(route_path(
350 350 'pullrequest_show',
351 351 repo_name=pull_request.target_repo.scm_instance().name,
352 352 pull_request_id=pull_request.pull_request_id))
353 353
354 354 response.mustcontain('Server-side pull request merging is disabled.')
355 355
356 356 assert_response = response.assert_response()
357 357 # for regular user without a merge permissions, we don't see it
358 358 assert_response.no_element_exists('#close-pull-request-action')
359 359
360 360 user_util.grant_user_permission_to_repo(
361 361 pull_request.target_repo,
362 362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 363 'repository.write')
364 364 response = self.app.get(route_path(
365 365 'pullrequest_show',
366 366 repo_name=pull_request.target_repo.scm_instance().name,
367 367 pull_request_id=pull_request.pull_request_id))
368 368
369 369 response.mustcontain('Server-side pull request merging is disabled.')
370 370
371 371 assert_response = response.assert_response()
372 372 # now regular user has a merge permissions, we have CLOSE button
373 373 assert_response.one_element_exists('#close-pull-request-action')
374 374
375 375 def test_show_invalid_commit_id(self, pr_util):
376 376 # Simulating invalid revisions which will cause a lookup error
377 377 pull_request = pr_util.create_pull_request()
378 378 pull_request.revisions = ['invalid']
379 379 Session().add(pull_request)
380 380 Session().commit()
381 381
382 382 response = self.app.get(route_path(
383 383 'pullrequest_show',
384 384 repo_name=pull_request.target_repo.scm_instance().name,
385 385 pull_request_id=pull_request.pull_request_id))
386 386
387 387 for commit_id in pull_request.revisions:
388 388 response.mustcontain(commit_id)
389 389
390 390 def test_show_invalid_source_reference(self, pr_util):
391 391 pull_request = pr_util.create_pull_request()
392 392 pull_request.source_ref = 'branch:b:invalid'
393 393 Session().add(pull_request)
394 394 Session().commit()
395 395
396 396 self.app.get(route_path(
397 397 'pullrequest_show',
398 398 repo_name=pull_request.target_repo.scm_instance().name,
399 399 pull_request_id=pull_request.pull_request_id))
400 400
401 401 def test_edit_title_description(self, pr_util, csrf_token):
402 402 pull_request = pr_util.create_pull_request()
403 403 pull_request_id = pull_request.pull_request_id
404 404
405 405 response = self.app.post(
406 406 route_path('pullrequest_update',
407 407 repo_name=pull_request.target_repo.repo_name,
408 408 pull_request_id=pull_request_id),
409 409 params={
410 410 'edit_pull_request': 'true',
411 411 'title': 'New title',
412 412 'description': 'New description',
413 413 'csrf_token': csrf_token})
414 414
415 415 assert_session_flash(
416 416 response, u'Pull request title & description updated.',
417 417 category='success')
418 418
419 419 pull_request = PullRequest.get(pull_request_id)
420 420 assert pull_request.title == 'New title'
421 421 assert pull_request.description == 'New description'
422 422
423 def test_edit_title_description(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
426
427 response = self.app.post(
428 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
431 params={
432 'edit_pull_request': 'true',
433 'title': 'New title {} {2} {foo}',
434 'description': 'New description',
435 'csrf_token': csrf_token})
436
437 assert_session_flash(
438 response, u'Pull request title & description updated.',
439 category='success')
440
441 pull_request = PullRequest.get(pull_request_id)
442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443
423 444 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 445 pull_request = pr_util.create_pull_request()
425 446 pull_request_id = pull_request.pull_request_id
426 447 repo_name = pull_request.target_repo.repo_name
427 448 pr_util.close()
428 449
429 450 response = self.app.post(
430 451 route_path('pullrequest_update',
431 452 repo_name=repo_name, pull_request_id=pull_request_id),
432 453 params={
433 454 'edit_pull_request': 'true',
434 455 'title': 'New title',
435 456 'description': 'New description',
436 457 'csrf_token': csrf_token}, status=200)
437 458 assert_session_flash(
438 459 response, u'Cannot update closed pull requests.',
439 460 category='error')
440 461
441 462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443 464
444 465 pull_request = pr_util.create_pull_request()
445 466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 467 Session().add(pull_request)
447 468 Session().commit()
448 469
449 470 pull_request_id = pull_request.pull_request_id
450 471
451 472 response = self.app.post(
452 473 route_path('pullrequest_update',
453 474 repo_name=pull_request.target_repo.repo_name,
454 475 pull_request_id=pull_request_id),
455 476 params={'update_commits': 'true', 'csrf_token': csrf_token})
456 477
457 478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 479 UpdateFailureReason.MISSING_SOURCE_REF])
459 480 assert_session_flash(response, expected_msg, category='error')
460 481
461 482 def test_missing_target_reference(self, pr_util, csrf_token):
462 483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 484 pull_request = pr_util.create_pull_request(
464 485 approved=True, mergeable=True)
465 486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 487 pull_request.target_ref = unicode_reference
467 488 Session().add(pull_request)
468 489 Session().commit()
469 490
470 491 pull_request_id = pull_request.pull_request_id
471 492 pull_request_url = route_path(
472 493 'pullrequest_show',
473 494 repo_name=pull_request.target_repo.repo_name,
474 495 pull_request_id=pull_request_id)
475 496
476 497 response = self.app.get(pull_request_url)
477 498 target_ref_id = 'invalid-branch'
478 499 merge_resp = MergeResponse(
479 500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 502 response.assert_response().element_contains(
482 503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483 504
484 505 def test_comment_and_close_pull_request_custom_message_approved(
485 506 self, pr_util, csrf_token, xhr_header):
486 507
487 508 pull_request = pr_util.create_pull_request(approved=True)
488 509 pull_request_id = pull_request.pull_request_id
489 510 author = pull_request.user_id
490 511 repo = pull_request.target_repo.repo_id
491 512
492 513 self.app.post(
493 514 route_path('pullrequest_comment_create',
494 515 repo_name=pull_request.target_repo.scm_instance().name,
495 516 pull_request_id=pull_request_id),
496 517 params={
497 518 'close_pull_request': '1',
498 519 'text': 'Closing a PR',
499 520 'csrf_token': csrf_token},
500 521 extra_environ=xhr_header,)
501 522
502 523 journal = UserLog.query()\
503 524 .filter(UserLog.user_id == author)\
504 525 .filter(UserLog.repository_id == repo) \
505 526 .order_by(UserLog.user_log_id.asc()) \
506 527 .all()
507 528 assert journal[-1].action == 'repo.pull_request.close'
508 529
509 530 pull_request = PullRequest.get(pull_request_id)
510 531 assert pull_request.is_closed()
511 532
512 533 status = ChangesetStatusModel().get_status(
513 534 pull_request.source_repo, pull_request=pull_request)
514 535 assert status == ChangesetStatus.STATUS_APPROVED
515 536 comments = ChangesetComment().query() \
516 537 .filter(ChangesetComment.pull_request == pull_request) \
517 538 .order_by(ChangesetComment.comment_id.asc())\
518 539 .all()
519 540 assert comments[-1].text == 'Closing a PR'
520 541
521 542 def test_comment_force_close_pull_request_rejected(
522 543 self, pr_util, csrf_token, xhr_header):
523 544 pull_request = pr_util.create_pull_request()
524 545 pull_request_id = pull_request.pull_request_id
525 546 PullRequestModel().update_reviewers(
526 547 pull_request_id, [
527 548 (1, ['reason'], False, 'reviewer', []),
528 549 (2, ['reason2'], False, 'reviewer', [])],
529 550 pull_request.author)
530 551 author = pull_request.user_id
531 552 repo = pull_request.target_repo.repo_id
532 553
533 554 self.app.post(
534 555 route_path('pullrequest_comment_create',
535 556 repo_name=pull_request.target_repo.scm_instance().name,
536 557 pull_request_id=pull_request_id),
537 558 params={
538 559 'close_pull_request': '1',
539 560 'csrf_token': csrf_token},
540 561 extra_environ=xhr_header)
541 562
542 563 pull_request = PullRequest.get(pull_request_id)
543 564
544 565 journal = UserLog.query()\
545 566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
546 567 .order_by(UserLog.user_log_id.asc()) \
547 568 .all()
548 569 assert journal[-1].action == 'repo.pull_request.close'
549 570
550 571 # check only the latest status, not the review status
551 572 status = ChangesetStatusModel().get_status(
552 573 pull_request.source_repo, pull_request=pull_request)
553 574 assert status == ChangesetStatus.STATUS_REJECTED
554 575
555 576 def test_comment_and_close_pull_request(
556 577 self, pr_util, csrf_token, xhr_header):
557 578 pull_request = pr_util.create_pull_request()
558 579 pull_request_id = pull_request.pull_request_id
559 580
560 581 response = self.app.post(
561 582 route_path('pullrequest_comment_create',
562 583 repo_name=pull_request.target_repo.scm_instance().name,
563 584 pull_request_id=pull_request.pull_request_id),
564 585 params={
565 586 'close_pull_request': 'true',
566 587 'csrf_token': csrf_token},
567 588 extra_environ=xhr_header)
568 589
569 590 assert response.json
570 591
571 592 pull_request = PullRequest.get(pull_request_id)
572 593 assert pull_request.is_closed()
573 594
574 595 # check only the latest status, not the review status
575 596 status = ChangesetStatusModel().get_status(
576 597 pull_request.source_repo, pull_request=pull_request)
577 598 assert status == ChangesetStatus.STATUS_REJECTED
578 599
579 600 def test_comment_and_close_pull_request_try_edit_comment(
580 601 self, pr_util, csrf_token, xhr_header
581 602 ):
582 603 pull_request = pr_util.create_pull_request()
583 604 pull_request_id = pull_request.pull_request_id
584 605 target_scm = pull_request.target_repo.scm_instance()
585 606 target_scm_name = target_scm.name
586 607
587 608 response = self.app.post(
588 609 route_path(
589 610 'pullrequest_comment_create',
590 611 repo_name=target_scm_name,
591 612 pull_request_id=pull_request_id,
592 613 ),
593 614 params={
594 615 'close_pull_request': 'true',
595 616 'csrf_token': csrf_token,
596 617 },
597 618 extra_environ=xhr_header)
598 619
599 620 assert response.json
600 621
601 622 pull_request = PullRequest.get(pull_request_id)
602 623 target_scm = pull_request.target_repo.scm_instance()
603 624 target_scm_name = target_scm.name
604 625 assert pull_request.is_closed()
605 626
606 627 # check only the latest status, not the review status
607 628 status = ChangesetStatusModel().get_status(
608 629 pull_request.source_repo, pull_request=pull_request)
609 630 assert status == ChangesetStatus.STATUS_REJECTED
610 631
611 632 for comment_id in response.json.keys():
612 633 test_text = 'test'
613 634 response = self.app.post(
614 635 route_path(
615 636 'pullrequest_comment_edit',
616 637 repo_name=target_scm_name,
617 638 pull_request_id=pull_request_id,
618 639 comment_id=comment_id,
619 640 ),
620 641 extra_environ=xhr_header,
621 642 params={
622 643 'csrf_token': csrf_token,
623 644 'text': test_text,
624 645 },
625 646 status=403,
626 647 )
627 648 assert response.status_int == 403
628 649
629 650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 651 pull_request = pr_util.create_pull_request()
631 652 target_scm = pull_request.target_repo.scm_instance()
632 653 target_scm_name = target_scm.name
633 654
634 655 response = self.app.post(
635 656 route_path(
636 657 'pullrequest_comment_create',
637 658 repo_name=target_scm_name,
638 659 pull_request_id=pull_request.pull_request_id),
639 660 params={
640 661 'csrf_token': csrf_token,
641 662 'text': 'init',
642 663 },
643 664 extra_environ=xhr_header,
644 665 )
645 666 assert response.json
646 667
647 668 for comment_id in response.json.keys():
648 669 assert comment_id
649 670 test_text = 'test'
650 671 self.app.post(
651 672 route_path(
652 673 'pullrequest_comment_edit',
653 674 repo_name=target_scm_name,
654 675 pull_request_id=pull_request.pull_request_id,
655 676 comment_id=comment_id,
656 677 ),
657 678 extra_environ=xhr_header,
658 679 params={
659 680 'csrf_token': csrf_token,
660 681 'text': test_text,
661 682 'version': '0',
662 683 },
663 684
664 685 )
665 686 text_form_db = ChangesetComment.query().filter(
666 687 ChangesetComment.comment_id == comment_id).first().text
667 688 assert test_text == text_form_db
668 689
669 690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 691 pull_request = pr_util.create_pull_request()
671 692 target_scm = pull_request.target_repo.scm_instance()
672 693 target_scm_name = target_scm.name
673 694
674 695 response = self.app.post(
675 696 route_path(
676 697 'pullrequest_comment_create',
677 698 repo_name=target_scm_name,
678 699 pull_request_id=pull_request.pull_request_id),
679 700 params={
680 701 'csrf_token': csrf_token,
681 702 'text': 'init',
682 703 },
683 704 extra_environ=xhr_header,
684 705 )
685 706 assert response.json
686 707
687 708 for comment_id in response.json.keys():
688 709 test_text = 'init'
689 710 response = self.app.post(
690 711 route_path(
691 712 'pullrequest_comment_edit',
692 713 repo_name=target_scm_name,
693 714 pull_request_id=pull_request.pull_request_id,
694 715 comment_id=comment_id,
695 716 ),
696 717 extra_environ=xhr_header,
697 718 params={
698 719 'csrf_token': csrf_token,
699 720 'text': test_text,
700 721 'version': '0',
701 722 },
702 723 status=404,
703 724
704 725 )
705 726 assert response.status_int == 404
706 727
707 728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 729 pull_request = pr_util.create_pull_request()
709 730 target_scm = pull_request.target_repo.scm_instance()
710 731 target_scm_name = target_scm.name
711 732
712 733 response = self.app.post(
713 734 route_path(
714 735 'pullrequest_comment_create',
715 736 repo_name=target_scm_name,
716 737 pull_request_id=pull_request.pull_request_id),
717 738 params={
718 739 'csrf_token': csrf_token,
719 740 'text': 'init',
720 741 },
721 742 extra_environ=xhr_header,
722 743 )
723 744 assert response.json
724 745 for comment_id in response.json.keys():
725 746 test_text = 'test'
726 747 self.app.post(
727 748 route_path(
728 749 'pullrequest_comment_edit',
729 750 repo_name=target_scm_name,
730 751 pull_request_id=pull_request.pull_request_id,
731 752 comment_id=comment_id,
732 753 ),
733 754 extra_environ=xhr_header,
734 755 params={
735 756 'csrf_token': csrf_token,
736 757 'text': test_text,
737 758 'version': '0',
738 759 },
739 760
740 761 )
741 762 test_text_v2 = 'test_v2'
742 763 response = self.app.post(
743 764 route_path(
744 765 'pullrequest_comment_edit',
745 766 repo_name=target_scm_name,
746 767 pull_request_id=pull_request.pull_request_id,
747 768 comment_id=comment_id,
748 769 ),
749 770 extra_environ=xhr_header,
750 771 params={
751 772 'csrf_token': csrf_token,
752 773 'text': test_text_v2,
753 774 'version': '0',
754 775 },
755 776 status=409,
756 777 )
757 778 assert response.status_int == 409
758 779
759 780 text_form_db = ChangesetComment.query().filter(
760 781 ChangesetComment.comment_id == comment_id).first().text
761 782
762 783 assert test_text == text_form_db
763 784 assert test_text_v2 != text_form_db
764 785
765 786 def test_comment_and_comment_edit_permissions_forbidden(
766 787 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 788 csrf_token, xhr_header):
768 789 pull_request = pr_util.create_pull_request(
769 790 author=user_admin.username, enable_notifications=False)
770 791 comment = CommentsModel().create(
771 792 text='test',
772 793 repo=pull_request.target_repo.scm_instance().name,
773 794 user=user_admin,
774 795 pull_request=pull_request,
775 796 )
776 797 response = self.app.post(
777 798 route_path(
778 799 'pullrequest_comment_edit',
779 800 repo_name=pull_request.target_repo.scm_instance().name,
780 801 pull_request_id=pull_request.pull_request_id,
781 802 comment_id=comment.comment_id,
782 803 ),
783 804 extra_environ=xhr_header,
784 805 params={
785 806 'csrf_token': csrf_token,
786 807 'text': 'test_text',
787 808 },
788 809 status=403,
789 810 )
790 811 assert response.status_int == 403
791 812
792 813 def test_create_pull_request(self, backend, csrf_token):
793 814 commits = [
794 815 {'message': 'ancestor'},
795 816 {'message': 'change'},
796 817 {'message': 'change2'},
797 818 ]
798 819 commit_ids = backend.create_master_repo(commits)
799 820 target = backend.create_repo(heads=['ancestor'])
800 821 source = backend.create_repo(heads=['change2'])
801 822
802 823 response = self.app.post(
803 824 route_path('pullrequest_create', repo_name=source.repo_name),
804 825 [
805 826 ('source_repo', source.repo_name),
806 827 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 828 ('target_repo', target.repo_name),
808 829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 830 ('common_ancestor', commit_ids['ancestor']),
810 831 ('pullrequest_title', 'Title'),
811 832 ('pullrequest_desc', 'Description'),
812 833 ('description_renderer', 'markdown'),
813 834 ('__start__', 'review_members:sequence'),
814 835 ('__start__', 'reviewer:mapping'),
815 836 ('user_id', '1'),
816 837 ('__start__', 'reasons:sequence'),
817 838 ('reason', 'Some reason'),
818 839 ('__end__', 'reasons:sequence'),
819 840 ('__start__', 'rules:sequence'),
820 841 ('__end__', 'rules:sequence'),
821 842 ('mandatory', 'False'),
822 843 ('__end__', 'reviewer:mapping'),
823 844 ('__end__', 'review_members:sequence'),
824 845 ('__start__', 'revisions:sequence'),
825 846 ('revisions', commit_ids['change']),
826 847 ('revisions', commit_ids['change2']),
827 848 ('__end__', 'revisions:sequence'),
828 849 ('user', ''),
829 850 ('csrf_token', csrf_token),
830 851 ],
831 852 status=302)
832 853
833 854 location = response.headers['Location']
834 855 pull_request_id = location.rsplit('/', 1)[1]
835 856 assert pull_request_id != 'new'
836 857 pull_request = PullRequest.get(int(pull_request_id))
837 858
838 859 # check that we have now both revisions
839 860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 863 assert pull_request.target_ref == expected_target_ref
843 864
844 865 def test_reviewer_notifications(self, backend, csrf_token):
845 866 # We have to use the app.post for this test so it will create the
846 867 # notifications properly with the new PR
847 868 commits = [
848 869 {'message': 'ancestor',
849 870 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 871 {'message': 'change',
851 872 'added': [FileNode('file_a', content='content_of_change')]},
852 873 {'message': 'change-child'},
853 874 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 875 'added': [
855 876 FileNode('file_B', content='content_of_ancestor_child')]},
856 877 {'message': 'ancestor-child-2'},
857 878 ]
858 879 commit_ids = backend.create_master_repo(commits)
859 880 target = backend.create_repo(heads=['ancestor-child'])
860 881 source = backend.create_repo(heads=['change'])
861 882
862 883 response = self.app.post(
863 884 route_path('pullrequest_create', repo_name=source.repo_name),
864 885 [
865 886 ('source_repo', source.repo_name),
866 887 ('source_ref', 'branch:default:' + commit_ids['change']),
867 888 ('target_repo', target.repo_name),
868 889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 890 ('common_ancestor', commit_ids['ancestor']),
870 891 ('pullrequest_title', 'Title'),
871 892 ('pullrequest_desc', 'Description'),
872 893 ('description_renderer', 'markdown'),
873 894 ('__start__', 'review_members:sequence'),
874 895 ('__start__', 'reviewer:mapping'),
875 896 ('user_id', '2'),
876 897 ('__start__', 'reasons:sequence'),
877 898 ('reason', 'Some reason'),
878 899 ('__end__', 'reasons:sequence'),
879 900 ('__start__', 'rules:sequence'),
880 901 ('__end__', 'rules:sequence'),
881 902 ('mandatory', 'False'),
882 903 ('__end__', 'reviewer:mapping'),
883 904 ('__end__', 'review_members:sequence'),
884 905 ('__start__', 'revisions:sequence'),
885 906 ('revisions', commit_ids['change']),
886 907 ('__end__', 'revisions:sequence'),
887 908 ('user', ''),
888 909 ('csrf_token', csrf_token),
889 910 ],
890 911 status=302)
891 912
892 913 location = response.headers['Location']
893 914
894 915 pull_request_id = location.rsplit('/', 1)[1]
895 916 assert pull_request_id != 'new'
896 917 pull_request = PullRequest.get(int(pull_request_id))
897 918
898 919 # Check that a notification was made
899 920 notifications = Notification.query()\
900 921 .filter(Notification.created_by == pull_request.author.user_id,
901 922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 923 Notification.subject.contains(
903 924 "requested a pull request review. !%s" % pull_request_id))
904 925 assert len(notifications.all()) == 1
905 926
906 927 # Change reviewers and check that a notification was made
907 928 PullRequestModel().update_reviewers(
908 929 pull_request.pull_request_id, [
909 930 (1, [], False, 'reviewer', [])
910 931 ],
911 932 pull_request.author)
912 933 assert len(notifications.all()) == 2
913 934
914 935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 936 commits = [
916 937 {'message': 'ancestor',
917 938 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 939 {'message': 'change',
919 940 'added': [FileNode('file_a', content='content_of_change')]},
920 941 {'message': 'change-child'},
921 942 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 943 'added': [
923 944 FileNode('file_B', content='content_of_ancestor_child')]},
924 945 {'message': 'ancestor-child-2'},
925 946 ]
926 947 commit_ids = backend.create_master_repo(commits)
927 948 target = backend.create_repo(heads=['ancestor-child'])
928 949 source = backend.create_repo(heads=['change'])
929 950
930 951 response = self.app.post(
931 952 route_path('pullrequest_create', repo_name=source.repo_name),
932 953 [
933 954 ('source_repo', source.repo_name),
934 955 ('source_ref', 'branch:default:' + commit_ids['change']),
935 956 ('target_repo', target.repo_name),
936 957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 958 ('common_ancestor', commit_ids['ancestor']),
938 959 ('pullrequest_title', 'Title'),
939 960 ('pullrequest_desc', 'Description'),
940 961 ('description_renderer', 'markdown'),
941 962 ('__start__', 'review_members:sequence'),
942 963 ('__start__', 'reviewer:mapping'),
943 964 ('user_id', '1'),
944 965 ('__start__', 'reasons:sequence'),
945 966 ('reason', 'Some reason'),
946 967 ('__end__', 'reasons:sequence'),
947 968 ('__start__', 'rules:sequence'),
948 969 ('__end__', 'rules:sequence'),
949 970 ('mandatory', 'False'),
950 971 ('__end__', 'reviewer:mapping'),
951 972 ('__end__', 'review_members:sequence'),
952 973 ('__start__', 'revisions:sequence'),
953 974 ('revisions', commit_ids['change']),
954 975 ('__end__', 'revisions:sequence'),
955 976 ('user', ''),
956 977 ('csrf_token', csrf_token),
957 978 ],
958 979 status=302)
959 980
960 981 location = response.headers['Location']
961 982
962 983 pull_request_id = location.rsplit('/', 1)[1]
963 984 assert pull_request_id != 'new'
964 985 pull_request = PullRequest.get(int(pull_request_id))
965 986
966 987 # target_ref has to point to the ancestor's commit_id in order to
967 988 # show the correct diff
968 989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 990 assert pull_request.target_ref == expected_target_ref
970 991
971 992 # Check generated diff contents
972 993 response = response.follow()
973 994 response.mustcontain(no=['content_of_ancestor'])
974 995 response.mustcontain(no=['content_of_ancestor-child'])
975 996 response.mustcontain('content_of_change')
976 997
977 998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 999 # Clear any previous calls to rcextensions
979 1000 rhodecode.EXTENSIONS.calls.clear()
980 1001
981 1002 pull_request = pr_util.create_pull_request(
982 1003 approved=True, mergeable=True)
983 1004 pull_request_id = pull_request.pull_request_id
984 1005 repo_name = pull_request.target_repo.scm_instance().name,
985 1006
986 1007 url = route_path('pullrequest_merge',
987 1008 repo_name=str(repo_name[0]),
988 1009 pull_request_id=pull_request_id)
989 1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990 1011
991 1012 pull_request = PullRequest.get(pull_request_id)
992 1013
993 1014 assert response.status_int == 200
994 1015 assert pull_request.is_closed()
995 1016 assert_pull_request_status(
996 1017 pull_request, ChangesetStatus.STATUS_APPROVED)
997 1018
998 1019 # Check the relevant log entries were added
999 1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 1021 actions = [log.action for log in user_logs]
1001 1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 1023 expected_actions = [
1003 1024 u'repo.pull_request.close',
1004 1025 u'repo.pull_request.merge',
1005 1026 u'repo.pull_request.comment.create'
1006 1027 ]
1007 1028 assert actions == expected_actions
1008 1029
1009 1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 1031 actions = [log for log in user_logs]
1011 1032 assert actions[-1].action == 'user.push'
1012 1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013 1034
1014 1035 # Check post_push rcextension was really executed
1015 1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 1037 assert len(push_calls) == 1
1017 1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 1039 assert last_call_kwargs['action'] == 'push'
1019 1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020 1041
1021 1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 1043 pull_request = pr_util.create_pull_request(mergeable=False)
1023 1044 pull_request_id = pull_request.pull_request_id
1024 1045 pull_request = PullRequest.get(pull_request_id)
1025 1046
1026 1047 response = self.app.post(
1027 1048 route_path('pullrequest_merge',
1028 1049 repo_name=pull_request.target_repo.scm_instance().name,
1029 1050 pull_request_id=pull_request.pull_request_id),
1030 1051 params={'csrf_token': csrf_token}).follow()
1031 1052
1032 1053 assert response.status_int == 200
1033 1054 response.mustcontain(
1034 1055 'Merge is not currently possible because of below failed checks.')
1035 1056 response.mustcontain('Server-side pull request merging is disabled.')
1036 1057
1037 1058 @pytest.mark.skip_backends('svn')
1038 1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 1060 pull_request = pr_util.create_pull_request(mergeable=True)
1040 1061 pull_request_id = pull_request.pull_request_id
1041 1062 repo_name = pull_request.target_repo.scm_instance().name
1042 1063
1043 1064 response = self.app.post(
1044 1065 route_path('pullrequest_merge',
1045 1066 repo_name=repo_name, pull_request_id=pull_request_id),
1046 1067 params={'csrf_token': csrf_token}).follow()
1047 1068
1048 1069 assert response.status_int == 200
1049 1070
1050 1071 response.mustcontain(
1051 1072 'Merge is not currently possible because of below failed checks.')
1052 1073 response.mustcontain('Pull request reviewer approval is pending.')
1053 1074
1054 1075 def test_merge_pull_request_renders_failure_reason(
1055 1076 self, user_regular, csrf_token, pr_util):
1056 1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 1078 pull_request_id = pull_request.pull_request_id
1058 1079 repo_name = pull_request.target_repo.scm_instance().name
1059 1080
1060 1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 1082 MergeFailureReason.PUSH_FAILED,
1062 1083 metadata={'target': 'shadow repo',
1063 1084 'merge_commit': 'xxx'})
1064 1085 model_patcher = mock.patch.multiple(
1065 1086 PullRequestModel,
1066 1087 merge_repo=mock.Mock(return_value=merge_resp),
1067 1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068 1089
1069 1090 with model_patcher:
1070 1091 response = self.app.post(
1071 1092 route_path('pullrequest_merge',
1072 1093 repo_name=repo_name,
1073 1094 pull_request_id=pull_request_id),
1074 1095 params={'csrf_token': csrf_token}, status=302)
1075 1096
1076 1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 1098 metadata={'target': 'shadow repo',
1078 1099 'merge_commit': 'xxx'})
1079 1100 assert_session_flash(response, merge_resp.merge_status_message)
1080 1101
1081 1102 def test_update_source_revision(self, backend, csrf_token):
1082 1103 commits = [
1083 1104 {'message': 'ancestor'},
1084 1105 {'message': 'change'},
1085 1106 {'message': 'change-2'},
1086 1107 ]
1087 1108 commit_ids = backend.create_master_repo(commits)
1088 1109 target = backend.create_repo(heads=['ancestor'])
1089 1110 source = backend.create_repo(heads=['change'])
1090 1111
1091 1112 # create pr from a in source to A in target
1092 1113 pull_request = PullRequest()
1093 1114
1094 1115 pull_request.source_repo = source
1095 1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097 1118
1098 1119 pull_request.target_repo = target
1099 1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101 1122
1102 1123 pull_request.revisions = [commit_ids['change']]
1103 1124 pull_request.title = u"Test"
1104 1125 pull_request.description = u"Description"
1105 1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 1128 Session().add(pull_request)
1108 1129 Session().commit()
1109 1130 pull_request_id = pull_request.pull_request_id
1110 1131
1111 1132 # source has ancestor - change - change-2
1112 1133 backend.pull_heads(source, heads=['change-2'])
1113 1134 target_repo_name = target.repo_name
1114 1135
1115 1136 # update PR
1116 1137 self.app.post(
1117 1138 route_path('pullrequest_update',
1118 1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120 1141
1121 1142 response = self.app.get(
1122 1143 route_path('pullrequest_show',
1123 1144 repo_name=target_repo_name,
1124 1145 pull_request_id=pull_request.pull_request_id))
1125 1146
1126 1147 assert response.status_int == 200
1127 1148 response.mustcontain('Pull request updated to')
1128 1149 response.mustcontain('with 1 added, 0 removed commits.')
1129 1150
1130 1151 # check that we have now both revisions
1131 1152 pull_request = PullRequest.get(pull_request_id)
1132 1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133 1154
1134 1155 def test_update_target_revision(self, backend, csrf_token):
1135 1156 commits = [
1136 1157 {'message': 'ancestor'},
1137 1158 {'message': 'change'},
1138 1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 1160 {'message': 'change-rebased'},
1140 1161 ]
1141 1162 commit_ids = backend.create_master_repo(commits)
1142 1163 target = backend.create_repo(heads=['ancestor'])
1143 1164 source = backend.create_repo(heads=['change'])
1144 1165
1145 1166 # create pr from a in source to A in target
1146 1167 pull_request = PullRequest()
1147 1168
1148 1169 pull_request.source_repo = source
1149 1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151 1172
1152 1173 pull_request.target_repo = target
1153 1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155 1176
1156 1177 pull_request.revisions = [commit_ids['change']]
1157 1178 pull_request.title = u"Test"
1158 1179 pull_request.description = u"Description"
1159 1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161 1182
1162 1183 Session().add(pull_request)
1163 1184 Session().commit()
1164 1185 pull_request_id = pull_request.pull_request_id
1165 1186
1166 1187 # target has ancestor - ancestor-new
1167 1188 # source has ancestor - ancestor-new - change-rebased
1168 1189 backend.pull_heads(target, heads=['ancestor-new'])
1169 1190 backend.pull_heads(source, heads=['change-rebased'])
1170 1191 target_repo_name = target.repo_name
1171 1192
1172 1193 # update PR
1173 1194 url = route_path('pullrequest_update',
1174 1195 repo_name=target_repo_name,
1175 1196 pull_request_id=pull_request_id)
1176 1197 self.app.post(url,
1177 1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 1199 status=200)
1179 1200
1180 1201 # check that we have now both revisions
1181 1202 pull_request = PullRequest.get(pull_request_id)
1182 1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185 1206
1186 1207 response = self.app.get(
1187 1208 route_path('pullrequest_show',
1188 1209 repo_name=target_repo_name,
1189 1210 pull_request_id=pull_request.pull_request_id))
1190 1211 assert response.status_int == 200
1191 1212 response.mustcontain('Pull request updated to')
1192 1213 response.mustcontain('with 1 added, 1 removed commits.')
1193 1214
1194 1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 1216 backend = backend_git
1196 1217 commits = [
1197 1218 {'message': 'master-commit-1'},
1198 1219 {'message': 'master-commit-2-change-1'},
1199 1220 {'message': 'master-commit-3-change-2'},
1200 1221
1201 1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 1223 {'message': 'feat-commit-2'},
1203 1224 ]
1204 1225 commit_ids = backend.create_master_repo(commits)
1205 1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 1227 source = backend.create_repo(heads=['feat-commit-2'])
1207 1228
1208 1229 # create pr from a in source to A in target
1209 1230 pull_request = PullRequest()
1210 1231 pull_request.source_repo = source
1211 1232
1212 1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 1234 branch=backend.default_branch_name,
1214 1235 commit_id=commit_ids['master-commit-3-change-2'])
1215 1236
1216 1237 pull_request.target_repo = target
1217 1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219 1240
1220 1241 pull_request.revisions = [
1221 1242 commit_ids['feat-commit-1'],
1222 1243 commit_ids['feat-commit-2']
1223 1244 ]
1224 1245 pull_request.title = u"Test"
1225 1246 pull_request.description = u"Description"
1226 1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 1249 Session().add(pull_request)
1229 1250 Session().commit()
1230 1251 pull_request_id = pull_request.pull_request_id
1231 1252
1232 1253 # PR is created, now we simulate a force-push into target,
1233 1254 # that drops a 2 last commits
1234 1255 vcsrepo = target.scm_instance()
1235 1256 vcsrepo.config.clear_section('hooks')
1236 1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 1258 target_repo_name = target.repo_name
1238 1259
1239 1260 # update PR
1240 1261 url = route_path('pullrequest_update',
1241 1262 repo_name=target_repo_name,
1242 1263 pull_request_id=pull_request_id)
1243 1264 self.app.post(url,
1244 1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 1266 status=200)
1246 1267
1247 1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 1269 assert response.status_int == 200
1249 1270 response.mustcontain('Pull request updated to')
1250 1271 response.mustcontain('with 0 added, 0 removed commits.')
1251 1272
1252 1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 1274 commits = [
1254 1275 {'message': 'ancestor'},
1255 1276 {'message': 'change'},
1256 1277 {'message': 'change-2'},
1257 1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 1279 {'message': 'change-rebased'},
1259 1280 ]
1260 1281 commit_ids = backend.create_master_repo(commits)
1261 1282 target = backend.create_repo(heads=['ancestor'])
1262 1283 source = backend.create_repo(heads=['change'])
1263 1284
1264 1285 # create pr from a in source to A in target
1265 1286 pull_request = PullRequest()
1266 1287 pull_request.source_repo = source
1267 1288
1268 1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 1291 pull_request.target_repo = target
1271 1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 1294 pull_request.revisions = [commit_ids['change']]
1274 1295 pull_request.title = u"Test"
1275 1296 pull_request.description = u"Description"
1276 1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 1299 Session().add(pull_request)
1279 1300 Session().commit()
1280 1301 pull_request_id = pull_request.pull_request_id
1281 1302
1282 1303 # target has ancestor - ancestor-new
1283 1304 # source has ancestor - ancestor-new - change-rebased
1284 1305 backend.pull_heads(target, heads=['ancestor-new'])
1285 1306 backend.pull_heads(source, heads=['change-rebased'])
1286 1307 target_repo_name = target.repo_name
1287 1308
1288 1309 # update PR
1289 1310 self.app.post(
1290 1311 route_path('pullrequest_update',
1291 1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 1314 status=200)
1294 1315
1295 1316 # Expect the target reference to be updated correctly
1296 1317 pull_request = PullRequest.get(pull_request_id)
1297 1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 1320 branch=backend.default_branch_name,
1300 1321 commit_id=commit_ids['ancestor-new'])
1301 1322 assert pull_request.target_ref == expected_target_ref
1302 1323
1303 1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 1325 branch_name = 'development'
1305 1326 commits = [
1306 1327 {'message': 'initial-commit'},
1307 1328 {'message': 'old-feature'},
1308 1329 {'message': 'new-feature', 'branch': branch_name},
1309 1330 ]
1310 1331 repo = backend_git.create_repo(commits)
1311 1332 repo_name = repo.repo_name
1312 1333 commit_ids = backend_git.commit_ids
1313 1334
1314 1335 pull_request = PullRequest()
1315 1336 pull_request.source_repo = repo
1316 1337 pull_request.target_repo = repo
1317 1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 1342 pull_request.revisions = [commit_ids['new-feature']]
1322 1343 pull_request.title = u"Test"
1323 1344 pull_request.description = u"Description"
1324 1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 1347 Session().add(pull_request)
1327 1348 Session().commit()
1328 1349
1329 1350 pull_request_id = pull_request.pull_request_id
1330 1351
1331 1352 vcs = repo.scm_instance()
1332 1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 1354 # NOTE(marcink): run GC to ensure the commits are gone
1334 1355 vcs.run_gc()
1335 1356
1336 1357 response = self.app.get(route_path(
1337 1358 'pullrequest_show',
1338 1359 repo_name=repo_name,
1339 1360 pull_request_id=pull_request_id))
1340 1361
1341 1362 assert response.status_int == 200
1342 1363
1343 1364 response.assert_response().element_contains(
1344 1365 '#changeset_compare_view_content .alert strong',
1345 1366 'Missing commits')
1346 1367 response.assert_response().element_contains(
1347 1368 '#changeset_compare_view_content .alert',
1348 1369 'This pull request cannot be displayed, because one or more'
1349 1370 ' commits no longer exist in the source repository.')
1350 1371
1351 1372 def test_strip_commits_from_pull_request(
1352 1373 self, backend, pr_util, csrf_token):
1353 1374 commits = [
1354 1375 {'message': 'initial-commit'},
1355 1376 {'message': 'old-feature'},
1356 1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 1378 ]
1358 1379 pull_request = pr_util.create_pull_request(
1359 1380 commits, target_head='initial-commit', source_head='new-feature',
1360 1381 revisions=['new-feature'])
1361 1382
1362 1383 vcs = pr_util.source_repository.scm_instance()
1363 1384 if backend.alias == 'git':
1364 1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 1386 else:
1366 1387 vcs.strip(pr_util.commit_ids['new-feature'])
1367 1388
1368 1389 response = self.app.get(route_path(
1369 1390 'pullrequest_show',
1370 1391 repo_name=pr_util.target_repository.repo_name,
1371 1392 pull_request_id=pull_request.pull_request_id))
1372 1393
1373 1394 assert response.status_int == 200
1374 1395
1375 1396 response.assert_response().element_contains(
1376 1397 '#changeset_compare_view_content .alert strong',
1377 1398 'Missing commits')
1378 1399 response.assert_response().element_contains(
1379 1400 '#changeset_compare_view_content .alert',
1380 1401 'This pull request cannot be displayed, because one or more'
1381 1402 ' commits no longer exist in the source repository.')
1382 1403 response.assert_response().element_contains(
1383 1404 '#update_commits',
1384 1405 'Update commits')
1385 1406
1386 1407 def test_strip_commits_and_update(
1387 1408 self, backend, pr_util, csrf_token):
1388 1409 commits = [
1389 1410 {'message': 'initial-commit'},
1390 1411 {'message': 'old-feature'},
1391 1412 {'message': 'new-feature', 'parents': ['old-feature']},
1392 1413 ]
1393 1414 pull_request = pr_util.create_pull_request(
1394 1415 commits, target_head='old-feature', source_head='new-feature',
1395 1416 revisions=['new-feature'], mergeable=True)
1396 1417 pr_id = pull_request.pull_request_id
1397 1418 target_repo_name = pull_request.target_repo.repo_name
1398 1419
1399 1420 vcs = pr_util.source_repository.scm_instance()
1400 1421 if backend.alias == 'git':
1401 1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 1423 else:
1403 1424 vcs.strip(pr_util.commit_ids['new-feature'])
1404 1425
1405 1426 url = route_path('pullrequest_update',
1406 1427 repo_name=target_repo_name,
1407 1428 pull_request_id=pr_id)
1408 1429 response = self.app.post(url,
1409 1430 params={'update_commits': 'true',
1410 1431 'csrf_token': csrf_token})
1411 1432
1412 1433 assert response.status_int == 200
1413 1434 assert response.body == '{"response": true, "redirect_url": null}'
1414 1435
1415 1436 # Make sure that after update, it won't raise 500 errors
1416 1437 response = self.app.get(route_path(
1417 1438 'pullrequest_show',
1418 1439 repo_name=target_repo_name,
1419 1440 pull_request_id=pr_id))
1420 1441
1421 1442 assert response.status_int == 200
1422 1443 response.assert_response().element_contains(
1423 1444 '#changeset_compare_view_content .alert strong',
1424 1445 'Missing commits')
1425 1446
1426 1447 def test_branch_is_a_link(self, pr_util):
1427 1448 pull_request = pr_util.create_pull_request()
1428 1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 1451 Session().add(pull_request)
1431 1452 Session().commit()
1432 1453
1433 1454 response = self.app.get(route_path(
1434 1455 'pullrequest_show',
1435 1456 repo_name=pull_request.target_repo.scm_instance().name,
1436 1457 pull_request_id=pull_request.pull_request_id))
1437 1458 assert response.status_int == 200
1438 1459
1439 1460 source = response.assert_response().get_element('.pr-source-info')
1440 1461 source_parent = source.getparent()
1441 1462 assert len(source_parent) == 1
1442 1463
1443 1464 target = response.assert_response().get_element('.pr-target-info')
1444 1465 target_parent = target.getparent()
1445 1466 assert len(target_parent) == 1
1446 1467
1447 1468 expected_origin_link = route_path(
1448 1469 'repo_commits',
1449 1470 repo_name=pull_request.source_repo.scm_instance().name,
1450 1471 params=dict(branch='origin'))
1451 1472 expected_target_link = route_path(
1452 1473 'repo_commits',
1453 1474 repo_name=pull_request.target_repo.scm_instance().name,
1454 1475 params=dict(branch='target'))
1455 1476 assert source_parent.attrib['href'] == expected_origin_link
1456 1477 assert target_parent.attrib['href'] == expected_target_link
1457 1478
1458 1479 def test_bookmark_is_not_a_link(self, pr_util):
1459 1480 pull_request = pr_util.create_pull_request()
1460 1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 1483 Session().add(pull_request)
1463 1484 Session().commit()
1464 1485
1465 1486 response = self.app.get(route_path(
1466 1487 'pullrequest_show',
1467 1488 repo_name=pull_request.target_repo.scm_instance().name,
1468 1489 pull_request_id=pull_request.pull_request_id))
1469 1490 assert response.status_int == 200
1470 1491
1471 1492 source = response.assert_response().get_element('.pr-source-info')
1472 1493 assert source.text.strip() == 'bookmark:origin'
1473 1494 assert source.getparent().attrib.get('href') is None
1474 1495
1475 1496 target = response.assert_response().get_element('.pr-target-info')
1476 1497 assert target.text.strip() == 'bookmark:target'
1477 1498 assert target.getparent().attrib.get('href') is None
1478 1499
1479 1500 def test_tag_is_not_a_link(self, pr_util):
1480 1501 pull_request = pr_util.create_pull_request()
1481 1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 1504 Session().add(pull_request)
1484 1505 Session().commit()
1485 1506
1486 1507 response = self.app.get(route_path(
1487 1508 'pullrequest_show',
1488 1509 repo_name=pull_request.target_repo.scm_instance().name,
1489 1510 pull_request_id=pull_request.pull_request_id))
1490 1511 assert response.status_int == 200
1491 1512
1492 1513 source = response.assert_response().get_element('.pr-source-info')
1493 1514 assert source.text.strip() == 'tag:origin'
1494 1515 assert source.getparent().attrib.get('href') is None
1495 1516
1496 1517 target = response.assert_response().get_element('.pr-target-info')
1497 1518 assert target.text.strip() == 'tag:target'
1498 1519 assert target.getparent().attrib.get('href') is None
1499 1520
1500 1521 @pytest.mark.parametrize('mergeable', [True, False])
1501 1522 def test_shadow_repository_link(
1502 1523 self, mergeable, pr_util, http_host_only_stub):
1503 1524 """
1504 1525 Check that the pull request summary page displays a link to the shadow
1505 1526 repository if the pull request is mergeable. If it is not mergeable
1506 1527 the link should not be displayed.
1507 1528 """
1508 1529 pull_request = pr_util.create_pull_request(
1509 1530 mergeable=mergeable, enable_notifications=False)
1510 1531 target_repo = pull_request.target_repo.scm_instance()
1511 1532 pr_id = pull_request.pull_request_id
1512 1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514 1535
1515 1536 response = self.app.get(route_path(
1516 1537 'pullrequest_show',
1517 1538 repo_name=target_repo.name,
1518 1539 pull_request_id=pr_id))
1519 1540
1520 1541 if mergeable:
1521 1542 response.assert_response().element_value_contains(
1522 1543 'input.pr-mergeinfo', shadow_url)
1523 1544 response.assert_response().element_value_contains(
1524 1545 'input.pr-mergeinfo ', 'pr-merge')
1525 1546 else:
1526 1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1527 1548
1528 1549
1529 1550 @pytest.mark.usefixtures('app')
1530 1551 @pytest.mark.backends("git", "hg")
1531 1552 class TestPullrequestsControllerDelete(object):
1532 1553 def test_pull_request_delete_button_permissions_admin(
1533 1554 self, autologin_user, user_admin, pr_util):
1534 1555 pull_request = pr_util.create_pull_request(
1535 1556 author=user_admin.username, enable_notifications=False)
1536 1557
1537 1558 response = self.app.get(route_path(
1538 1559 'pullrequest_show',
1539 1560 repo_name=pull_request.target_repo.scm_instance().name,
1540 1561 pull_request_id=pull_request.pull_request_id))
1541 1562
1542 1563 response.mustcontain('id="delete_pullrequest"')
1543 1564 response.mustcontain('Confirm to delete this pull request')
1544 1565
1545 1566 def test_pull_request_delete_button_permissions_owner(
1546 1567 self, autologin_regular_user, user_regular, pr_util):
1547 1568 pull_request = pr_util.create_pull_request(
1548 1569 author=user_regular.username, enable_notifications=False)
1549 1570
1550 1571 response = self.app.get(route_path(
1551 1572 'pullrequest_show',
1552 1573 repo_name=pull_request.target_repo.scm_instance().name,
1553 1574 pull_request_id=pull_request.pull_request_id))
1554 1575
1555 1576 response.mustcontain('id="delete_pullrequest"')
1556 1577 response.mustcontain('Confirm to delete this pull request')
1557 1578
1558 1579 def test_pull_request_delete_button_permissions_forbidden(
1559 1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 1581 pull_request = pr_util.create_pull_request(
1561 1582 author=user_admin.username, enable_notifications=False)
1562 1583
1563 1584 response = self.app.get(route_path(
1564 1585 'pullrequest_show',
1565 1586 repo_name=pull_request.target_repo.scm_instance().name,
1566 1587 pull_request_id=pull_request.pull_request_id))
1567 1588 response.mustcontain(no=['id="delete_pullrequest"'])
1568 1589 response.mustcontain(no=['Confirm to delete this pull request'])
1569 1590
1570 1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 1593 user_util):
1573 1594
1574 1595 pull_request = pr_util.create_pull_request(
1575 1596 author=user_admin.username, enable_notifications=False)
1576 1597
1577 1598 user_util.grant_user_permission_to_repo(
1578 1599 pull_request.target_repo, user_regular,
1579 1600 'repository.write')
1580 1601
1581 1602 response = self.app.get(route_path(
1582 1603 'pullrequest_show',
1583 1604 repo_name=pull_request.target_repo.scm_instance().name,
1584 1605 pull_request_id=pull_request.pull_request_id))
1585 1606
1586 1607 response.mustcontain('id="open_edit_pullrequest"')
1587 1608 response.mustcontain('id="delete_pullrequest"')
1588 1609 response.mustcontain(no=['Confirm to delete this pull request'])
1589 1610
1590 1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592 1613
1593 1614 pull_request = pr_util.create_pull_request(
1594 1615 author=user_admin.username, enable_notifications=False)
1595 1616
1596 1617 self.app.post(
1597 1618 route_path(
1598 1619 'pullrequest_comment_delete',
1599 1620 repo_name=pull_request.target_repo.scm_instance().name,
1600 1621 pull_request_id=pull_request.pull_request_id,
1601 1622 comment_id=1024404),
1602 1623 extra_environ=xhr_header,
1603 1624 params={'csrf_token': csrf_token},
1604 1625 status=404
1605 1626 )
1606 1627
1607 1628 def test_delete_comment(
1608 1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609 1630
1610 1631 pull_request = pr_util.create_pull_request(
1611 1632 author=user_admin.username, enable_notifications=False)
1612 1633 comment = pr_util.create_comment()
1613 1634 comment_id = comment.comment_id
1614 1635
1615 1636 response = self.app.post(
1616 1637 route_path(
1617 1638 'pullrequest_comment_delete',
1618 1639 repo_name=pull_request.target_repo.scm_instance().name,
1619 1640 pull_request_id=pull_request.pull_request_id,
1620 1641 comment_id=comment_id),
1621 1642 extra_environ=xhr_header,
1622 1643 params={'csrf_token': csrf_token},
1623 1644 status=200
1624 1645 )
1625 1646 assert response.body == 'true'
1626 1647
1627 1648 @pytest.mark.parametrize('url_type', [
1628 1649 'pullrequest_new',
1629 1650 'pullrequest_create',
1630 1651 'pullrequest_update',
1631 1652 'pullrequest_merge',
1632 1653 ])
1633 1654 def test_pull_request_is_forbidden_on_archived_repo(
1634 1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1635 1656
1636 1657 # create a temporary repo
1637 1658 source = user_util.create_repo(repo_type=backend.alias)
1638 1659 repo_name = source.repo_name
1639 1660 repo = Repository.get_by_repo_name(repo_name)
1640 1661 repo.archived = True
1641 1662 Session().commit()
1642 1663
1643 1664 response = self.app.get(
1644 1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645 1666
1646 1667 msg = 'Action not supported for archived repository.'
1647 1668 assert_session_flash(response, msg)
1648 1669
1649 1670
1650 1671 def assert_pull_request_status(pull_request, expected_status):
1651 1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 1673 assert status == expected_status
1653 1674
1654 1675
1655 1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 1677 @pytest.mark.usefixtures("autologin_user")
1657 1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1502 +1,1501 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84 84
85 85 if (!(this instanceof CommentForm)) {
86 86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 87 }
88 88
89 89 // bind the element instance to our Form
90 90 $(formElement).get(0).CommentForm = this;
91 91
92 92 this.withLineNo = function(selector) {
93 93 var lineNo = this.lineNo;
94 94 if (lineNo === undefined) {
95 95 return selector
96 96 } else {
97 97 return selector + '_' + lineNo;
98 98 }
99 99 };
100 100
101 101 this.commitId = commitId;
102 102 this.pullRequestId = pullRequestId;
103 103 this.lineNo = lineNo;
104 104 this.initAutocompleteActions = initAutocompleteActions;
105 105
106 106 this.previewButton = this.withLineNo('#preview-btn');
107 107 this.previewContainer = this.withLineNo('#preview-container');
108 108
109 109 this.previewBoxSelector = this.withLineNo('#preview-box');
110 110
111 111 this.editButton = this.withLineNo('#edit-btn');
112 112 this.editContainer = this.withLineNo('#edit-container');
113 113 this.cancelButton = this.withLineNo('#cancel-btn');
114 114 this.commentType = this.withLineNo('#comment_type');
115 115
116 116 this.resolvesId = null;
117 117 this.resolvesActionId = null;
118 118
119 119 this.closesPr = '#close_pull_request';
120 120
121 121 this.cmBox = this.withLineNo('#text');
122 122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123 123
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 127
128 128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 129 this.submitButtonText = this.submitButton.val();
130 130
131 131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 132 this.submitDraftButtonText = this.submitDraftButton.val();
133 133
134 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 135 {'repo_name': templateContext.repo_name,
136 136 'commit_id': templateContext.commit_data.commit_id});
137 137
138 138 if (edit){
139 139 this.submitDraftButton.hide();
140 140 this.submitButtonText = _gettext('Update Comment');
141 141 $(this.commentType).prop('disabled', true);
142 142 $(this.commentType).addClass('disabled');
143 143 var editInfo =
144 144 '';
145 145 $(editInfo).insertBefore($(this.editButton).parent());
146 146 }
147 147
148 148 if (resolvesCommentId){
149 149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 151 $(this.commentType).prop('disabled', true);
152 152 $(this.commentType).addClass('disabled');
153 153
154 154 // disable select
155 155 setTimeout(function() {
156 156 $(self.statusChange).select2('readonly', true);
157 157 }, 10);
158 158
159 159 var resolvedInfo = (
160 160 '<li class="resolve-action">' +
161 161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 163 '</li>'
164 164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 166 }
167 167
168 168 // based on commitId, or pullRequestId decide where do we submit
169 169 // out data
170 170 if (this.commitId){
171 171 var pyurl = 'repo_commit_comment_create';
172 172 if(edit){
173 173 pyurl = 'repo_commit_comment_edit';
174 174 }
175 175 this.submitUrl = pyroutes.url(pyurl,
176 176 {'repo_name': templateContext.repo_name,
177 177 'commit_id': this.commitId,
178 178 'comment_id': comment_id});
179 179 this.selfUrl = pyroutes.url('repo_commit',
180 180 {'repo_name': templateContext.repo_name,
181 181 'commit_id': this.commitId});
182 182
183 183 } else if (this.pullRequestId) {
184 184 var pyurl = 'pullrequest_comment_create';
185 185 if(edit){
186 186 pyurl = 'pullrequest_comment_edit';
187 187 }
188 188 this.submitUrl = pyroutes.url(pyurl,
189 189 {'repo_name': templateContext.repo_name,
190 190 'pull_request_id': this.pullRequestId,
191 191 'comment_id': comment_id});
192 192 this.selfUrl = pyroutes.url('pullrequest_show',
193 193 {'repo_name': templateContext.repo_name,
194 194 'pull_request_id': this.pullRequestId});
195 195
196 196 } else {
197 197 throw new Error(
198 198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 199 }
200 200
201 201 // FUNCTIONS and helpers
202 202 var self = this;
203 203
204 204 this.isInline = function(){
205 205 return this.lineNo && this.lineNo != 'general';
206 206 };
207 207
208 208 this.getCmInstance = function(){
209 209 return this.cm
210 210 };
211 211
212 212 this.setPlaceholder = function(placeholder) {
213 213 var cm = this.getCmInstance();
214 214 if (cm){
215 215 cm.setOption('placeholder', placeholder);
216 216 }
217 217 };
218 218
219 219 this.getCommentStatus = function() {
220 220 return $(this.submitForm).find(this.statusChange).val();
221 221 };
222 222
223 223 this.getCommentType = function() {
224 224 return $(this.submitForm).find(this.commentType).val();
225 225 };
226 226
227 227 this.getDraftState = function () {
228 228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 229 var data = $(submitterElem).data('isDraft');
230 230 return data
231 231 }
232 232
233 233 this.getResolvesId = function() {
234 234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 235 };
236 236
237 237 this.getClosePr = function() {
238 238 return $(this.submitForm).find(this.closesPr).val() || null;
239 239 };
240 240
241 241 this.markCommentResolved = function(resolvedCommentId){
242 242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 244 };
245 245
246 246 this.isAllowedToSubmit = function() {
247 247 var commentDisabled = $(this.submitButton).prop('disabled');
248 248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 249 return !commentDisabled && !draftDisabled;
250 250 };
251 251
252 252 this.initStatusChangeSelector = function(){
253 253 var formatChangeStatus = function(state, escapeMarkup) {
254 254 var originalOption = state.element;
255 255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 256 return tmpl
257 257 };
258 258 var formatResult = function(result, container, query, escapeMarkup) {
259 259 return formatChangeStatus(result, escapeMarkup);
260 260 };
261 261
262 262 var formatSelection = function(data, container, escapeMarkup) {
263 263 return formatChangeStatus(data, escapeMarkup);
264 264 };
265 265
266 266 $(this.submitForm).find(this.statusChange).select2({
267 267 placeholder: _gettext('Status Review'),
268 268 formatResult: formatResult,
269 269 formatSelection: formatSelection,
270 270 containerCssClass: "drop-menu status_box_menu",
271 271 dropdownCssClass: "drop-menu-dropdown",
272 272 dropdownAutoWidth: true,
273 273 minimumResultsForSearch: -1
274 274 });
275 275
276 276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 277 var status = self.getCommentStatus();
278 278
279 279 if (status && !self.isInline()) {
280 280 $(self.submitButton).prop('disabled', false);
281 281 $(self.submitDraftButton).prop('disabled', false);
282 282 }
283 283
284 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 285 self.setPlaceholder(placeholderText)
286 286 })
287 287 };
288 288
289 289 // reset the comment form into it's original state
290 290 this.resetCommentFormState = function(content) {
291 291 content = content || '';
292 292
293 293 $(this.editContainer).show();
294 294 $(this.editButton).parent().addClass('active');
295 295
296 296 $(this.previewContainer).hide();
297 297 $(this.previewButton).parent().removeClass('active');
298 298
299 299 this.setActionButtonsDisabled(true);
300 300 self.cm.setValue(content);
301 301 self.cm.setOption("readOnly", false);
302 302
303 303 if (this.resolvesId) {
304 304 // destroy the resolve action
305 305 $(this.resolvesId).parent().remove();
306 306 }
307 307 // reset closingPR flag
308 308 $('.close-pr-input').remove();
309 309
310 310 $(this.statusChange).select2('readonly', false);
311 311 };
312 312
313 313 this.globalSubmitSuccessCallback = function(comment){
314 314 // default behaviour is to call GLOBAL hook, if it's registered.
315 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 316 commentFormGlobalSubmitSuccessCallback(comment);
317 317 }
318 318 };
319 319
320 320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 322 };
323 323
324 324 // overwrite a submitHandler, we need to do it for inline comments
325 325 this.setHandleFormSubmit = function(callback) {
326 326 this.handleFormSubmit = callback;
327 327 };
328 328
329 329 // overwrite a submitSuccessHandler
330 330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 331 this.globalSubmitSuccessCallback = callback;
332 332 };
333 333
334 334 // default handler for for submit for main comments
335 335 this.handleFormSubmit = function() {
336 336 var text = self.cm.getValue();
337 337 var status = self.getCommentStatus();
338 338 var commentType = self.getCommentType();
339 339 var isDraft = self.getDraftState();
340 340 var resolvesCommentId = self.getResolvesId();
341 341 var closePullRequest = self.getClosePr();
342 342
343 343 if (text === "" && !status) {
344 344 return;
345 345 }
346 346
347 347 var excludeCancelBtn = false;
348 348 var submitEvent = true;
349 349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 350 self.cm.setOption("readOnly", true);
351 351
352 352 var postData = {
353 353 'text': text,
354 354 'changeset_status': status,
355 355 'comment_type': commentType,
356 356 'csrf_token': CSRF_TOKEN
357 357 };
358 358
359 359 if (resolvesCommentId) {
360 360 postData['resolves_comment_id'] = resolvesCommentId;
361 361 }
362 362
363 363 if (closePullRequest) {
364 364 postData['close_pull_request'] = true;
365 365 }
366 366
367 367 // submitSuccess for general comments
368 368 var submitSuccessCallback = function(json_data) {
369 369 // reload page if we change status for single commit.
370 370 if (status && self.commitId) {
371 371 location.reload(true);
372 372 } else {
373 373 // inject newly created comments, json_data is {<comment_id>: {}}
374 374 Rhodecode.comments.attachGeneralComment(json_data)
375 375
376 376 self.resetCommentFormState();
377 377 timeagoActivate();
378 378 tooltipActivate();
379 379
380 380 // mark visually which comment was resolved
381 381 if (resolvesCommentId) {
382 382 self.markCommentResolved(resolvesCommentId);
383 383 }
384 384 }
385 385
386 386 // run global callback on submit
387 387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388 388
389 389 };
390 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 391 var prefix = "Error while submitting comment.\n"
392 392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 393 ajaxErrorSwal(message);
394 394 self.resetCommentFormState(text);
395 395 };
396 396 self.submitAjaxPOST(
397 397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 398 };
399 399
400 400 this.previewSuccessCallback = function(o) {
401 401 $(self.previewBoxSelector).html(o);
402 402 $(self.previewBoxSelector).removeClass('unloaded');
403 403
404 404 // swap buttons, making preview active
405 405 $(self.previewButton).parent().addClass('active');
406 406 $(self.editButton).parent().removeClass('active');
407 407
408 408 // unlock buttons
409 409 self.setActionButtonsDisabled(false);
410 410 };
411 411
412 412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 413 excludeCancelBtn = excludeCancelBtn || false;
414 414 submitEvent = submitEvent || false;
415 415
416 416 $(this.editButton).prop('disabled', state);
417 417 $(this.previewButton).prop('disabled', state);
418 418
419 419 if (!excludeCancelBtn) {
420 420 $(this.cancelButton).prop('disabled', state);
421 421 }
422 422
423 423 var submitState = state;
424 424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 425 // if the value of commit review status is set, we allow
426 426 // submit button, but only on Main form, isInline means inline
427 427 submitState = false
428 428 }
429 429
430 430 $(this.submitButton).prop('disabled', submitState);
431 431 $(this.submitDraftButton).prop('disabled', submitState);
432 432
433 433 if (submitEvent) {
434 434 var isDraft = self.getDraftState();
435 435
436 436 if (isDraft) {
437 437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 438 } else {
439 439 $(this.submitButton).val(_gettext('Submitting...'));
440 440 }
441 441
442 442 } else {
443 443 $(this.submitButton).val(this.submitButtonText);
444 444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 445 }
446 446
447 447 };
448 448
449 449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 450 var excludeCancelBtn = true;
451 451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452 452
453 453 // anonymous users don't have access to initialized CM instance
454 454 if (this.cm !== undefined){
455 455 this.cm.on('change', function(cMirror) {
456 456 if (cMirror.getValue() === "") {
457 457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 458 } else {
459 459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 460 }
461 461 });
462 462 }
463 463
464 464 $(this.editButton).on('click', function(e) {
465 465 e.preventDefault();
466 466
467 467 $(self.previewButton).parent().removeClass('active');
468 468 $(self.previewContainer).hide();
469 469
470 470 $(self.editButton).parent().addClass('active');
471 471 $(self.editContainer).show();
472 472
473 473 });
474 474
475 475 $(this.previewButton).on('click', function(e) {
476 476 e.preventDefault();
477 477 var text = self.cm.getValue();
478 478
479 479 if (text === "") {
480 480 return;
481 481 }
482 482
483 483 var postData = {
484 484 'text': text,
485 485 'renderer': templateContext.visual.default_renderer,
486 486 'csrf_token': CSRF_TOKEN
487 487 };
488 488
489 489 // lock ALL buttons on preview
490 490 self.setActionButtonsDisabled(true);
491 491
492 492 $(self.previewBoxSelector).addClass('unloaded');
493 493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494 494
495 495 $(self.editContainer).hide();
496 496 $(self.previewContainer).show();
497 497
498 498 // by default we reset state of comment preserving the text
499 499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 500 var prefix = "Error while preview of comment.\n"
501 501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 502 ajaxErrorSwal(message);
503 503
504 504 self.resetCommentFormState(text)
505 505 };
506 506 self.submitAjaxPOST(
507 507 self.previewUrl, postData, self.previewSuccessCallback,
508 508 previewFailCallback);
509 509
510 510 $(self.previewButton).parent().addClass('active');
511 511 $(self.editButton).parent().removeClass('active');
512 512 });
513 513
514 514 $(this.submitForm).submit(function(e) {
515 515 e.preventDefault();
516 516 var allowedToSubmit = self.isAllowedToSubmit();
517 517 if (!allowedToSubmit){
518 518 return false;
519 519 }
520 520
521 521 self.handleFormSubmit();
522 522 });
523 523
524 524 }
525 525
526 526 return CommentForm;
527 527 });
528 528
529 529 /* selector for comment versions */
530 530 var initVersionSelector = function(selector, initialData) {
531 531
532 532 var formatResult = function(result, container, query, escapeMarkup) {
533 533
534 534 return renderTemplate('commentVersion', {
535 535 show_disabled: true,
536 536 version: result.comment_version,
537 537 user_name: result.comment_author_username,
538 538 gravatar_url: result.comment_author_gravatar,
539 539 size: 16,
540 540 timeago_component: result.comment_created_on,
541 541 })
542 542 };
543 543
544 544 $(selector).select2({
545 545 placeholder: "Edited",
546 546 containerCssClass: "drop-menu-comment-history",
547 547 dropdownCssClass: "drop-menu-dropdown",
548 548 dropdownAutoWidth: true,
549 549 minimumResultsForSearch: -1,
550 550 data: initialData,
551 551 formatResult: formatResult,
552 552 });
553 553
554 554 $(selector).on('select2-selecting', function (e) {
555 555 // hide the mast as we later do preventDefault()
556 556 $("#select2-drop-mask").click();
557 557 e.preventDefault();
558 558 e.choice.action();
559 559 });
560 560
561 561 $(selector).on("select2-open", function() {
562 562 timeagoActivate();
563 563 });
564 564 };
565 565
566 566 /* comments controller */
567 567 var CommentsController = function() {
568 568 var mainComment = '#text';
569 569 var self = this;
570 570
571 571 this.showVersion = function (comment_id, comment_history_id) {
572 572
573 573 var historyViewUrl = pyroutes.url(
574 574 'repo_commit_comment_history_view',
575 575 {
576 576 'repo_name': templateContext.repo_name,
577 577 'commit_id': comment_id,
578 578 'comment_history_id': comment_history_id,
579 579 }
580 580 );
581 581 successRenderCommit = function (data) {
582 582 SwalNoAnimation.fire({
583 583 html: data,
584 584 title: '',
585 585 });
586 586 };
587 587 failRenderCommit = function () {
588 588 SwalNoAnimation.fire({
589 589 html: 'Error while loading comment history',
590 590 title: '',
591 591 });
592 592 };
593 593 _submitAjaxPOST(
594 594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 595 successRenderCommit,
596 596 failRenderCommit
597 597 );
598 598 };
599 599
600 600 this.getLineNumber = function(node) {
601 601 var $node = $(node);
602 602 var lineNo = $node.closest('td').attr('data-line-no');
603 603 if (lineNo === undefined && $node.data('commentInline')){
604 604 lineNo = $node.data('commentLineNo')
605 605 }
606 606
607 607 return lineNo
608 608 };
609 609
610 610 this.scrollToComment = function(node, offset, outdated) {
611 611 if (offset === undefined) {
612 612 offset = 0;
613 613 }
614 614 var outdated = outdated || false;
615 615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616 616
617 617 if (!node) {
618 618 node = $('.comment-selected');
619 619 if (!node.length) {
620 620 node = $('comment-current')
621 621 }
622 622 }
623 623
624 624 $wrapper = $(node).closest('div.comment');
625 625
626 626 // show hidden comment when referenced.
627 627 if (!$wrapper.is(':visible')){
628 628 $wrapper.show();
629 629 }
630 630
631 631 $comment = $(node).closest(klass);
632 632 $comments = $(klass);
633 633
634 634 $('.comment-selected').removeClass('comment-selected');
635 635
636 636 var nextIdx = $(klass).index($comment) + offset;
637 637 if (nextIdx >= $comments.length) {
638 638 nextIdx = 0;
639 639 }
640 640 var $next = $(klass).eq(nextIdx);
641 641
642 642 var $cb = $next.closest('.cb');
643 643 $cb.removeClass('cb-collapsed');
644 644
645 645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 646 $filediffCollapseState.prop('checked', false);
647 647 $next.addClass('comment-selected');
648 648 scrollToElement($next);
649 649 return false;
650 650 };
651 651
652 652 this.nextComment = function(node) {
653 653 return self.scrollToComment(node, 1);
654 654 };
655 655
656 656 this.prevComment = function(node) {
657 657 return self.scrollToComment(node, -1);
658 658 };
659 659
660 660 this.nextOutdatedComment = function(node) {
661 661 return self.scrollToComment(node, 1, true);
662 662 };
663 663
664 664 this.prevOutdatedComment = function(node) {
665 665 return self.scrollToComment(node, -1, true);
666 666 };
667 667
668 668 this.cancelComment = function (node) {
669 669 var $node = $(node);
670 670 var edit = $(this).attr('edit');
671 671 var $inlineComments = $node.closest('div.inline-comments');
672 672
673 673 if (edit) {
674 674 var $general_comments = null;
675 675 if (!$inlineComments.length) {
676 676 $general_comments = $('#comments');
677 677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 678 // show hidden general comment form
679 679 $('#cb-comment-general-form-placeholder').show();
680 680 } else {
681 681 var $comment = $inlineComments.find('div.comment:hidden');
682 682 }
683 683 $comment.show();
684 684 }
685 685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 686 $replyWrapper.removeClass('comment-form-active');
687 687
688 688 var lastComment = $inlineComments.find('.comment-inline').last();
689 689 if ($(lastComment).hasClass('comment-outdated')) {
690 690 $replyWrapper.hide();
691 691 }
692 692
693 693 $node.closest('.comment-inline-form').remove();
694 694 return false;
695 695 };
696 696
697 697 this._deleteComment = function(node) {
698 698 var $node = $(node);
699 699 var $td = $node.closest('td');
700 700 var $comment = $node.closest('.comment');
701 701 var comment_id = $($comment).data('commentId');
702 702 var isDraft = $($comment).data('commentDraft');
703 703
704 704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 705 var commitId = templateContext.commit_data.commit_id;
706 706
707 707 if (pullRequestId) {
708 708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 709 } else if (commitId) {
710 710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 711 }
712 712
713 713 var postData = {
714 714 'csrf_token': CSRF_TOKEN
715 715 };
716 716
717 717 $comment.addClass('comment-deleting');
718 718 $comment.hide('fast');
719 719
720 720 var success = function(response) {
721 721 $comment.remove();
722 722
723 723 if (window.updateSticky !== undefined) {
724 724 // potentially our comments change the active window size, so we
725 725 // notify sticky elements
726 726 updateSticky()
727 727 }
728 728
729 729 if (window.refreshAllComments !== undefined && !isDraft) {
730 730 // if we have this handler, run it, and refresh all comments boxes
731 731 refreshAllComments()
732 732 }
733 733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 734 // if we have this handler, run it, and refresh all comments boxes
735 735 refreshDraftComments();
736 736 }
737 737 return false;
738 738 };
739 739
740 740 var failure = function(jqXHR, textStatus, errorThrown) {
741 741 var prefix = "Error while deleting this comment.\n"
742 742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
743 743 ajaxErrorSwal(message);
744 744
745 745 $comment.show('fast');
746 746 $comment.removeClass('comment-deleting');
747 747 return false;
748 748 };
749 749 ajaxPOST(url, postData, success, failure);
750 750
751 751 }
752 752
753 753 this.deleteComment = function(node) {
754 754 var $comment = $(node).closest('.comment');
755 755 var comment_id = $comment.attr('data-comment-id');
756 756
757 757 SwalNoAnimation.fire({
758 758 title: 'Delete this comment?',
759 759 icon: 'warning',
760 760 showCancelButton: true,
761 761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
762 762
763 763 }).then(function(result) {
764 764 if (result.value) {
765 765 self._deleteComment(node);
766 766 }
767 767 })
768 768 };
769 769
770 770 this._finalizeDrafts = function(commentIds) {
771 771
772 772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 773 var commitId = templateContext.commit_data.commit_id;
774 774
775 775 if (pullRequestId) {
776 776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 777 } else if (commitId) {
778 778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 779 }
780 780
781 781 // remove the drafts so we can lock them before submit.
782 782 $.each(commentIds, function(idx, val){
783 783 $('#comment-{0}'.format(val)).remove();
784 784 })
785 785
786 786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787 787
788 788 var submitSuccessCallback = function(json_data) {
789 789 self.attachInlineComment(json_data);
790 790
791 791 if (window.refreshDraftComments !== undefined) {
792 792 // if we have this handler, run it, and refresh all comments boxes
793 793 refreshDraftComments()
794 794 }
795 795
796 796 return false;
797 797 };
798 798
799 799 ajaxPOST(url, postData, submitSuccessCallback)
800 800
801 801 }
802 802
803 803 this.finalizeDrafts = function(commentIds, callback) {
804 804
805 805 SwalNoAnimation.fire({
806 806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 807 icon: 'warning',
808 808 showCancelButton: true,
809 809 confirmButtonText: _gettext('Yes'),
810 810
811 811 }).then(function(result) {
812 812 if (result.value) {
813 813 if (callback !== undefined) {
814 814 callback(result)
815 815 }
816 816 self._finalizeDrafts(commentIds);
817 817 }
818 818 })
819 819 };
820 820
821 821 this.toggleWideMode = function (node) {
822 822
823 823 if ($('#content').hasClass('wrapper')) {
824 824 $('#content').removeClass("wrapper");
825 825 $('#content').addClass("wide-mode-wrapper");
826 826 $(node).addClass('btn-success');
827 827 return true
828 828 } else {
829 829 $('#content').removeClass("wide-mode-wrapper");
830 830 $('#content').addClass("wrapper");
831 831 $(node).removeClass('btn-success');
832 832 return false
833 833 }
834 834
835 835 };
836 836
837 837 /**
838 838 * Turn off/on all comments in file diff
839 839 */
840 840 this.toggleDiffComments = function(node) {
841 841 // Find closes filediff container
842 842 var $filediff = $(node).closest('.filediff');
843 843 if ($(node).hasClass('toggle-on')) {
844 844 var show = false;
845 845 } else if ($(node).hasClass('toggle-off')) {
846 846 var show = true;
847 847 }
848 848
849 849 // Toggle each individual comment block, so we can un-toggle single ones
850 850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 851 self.toggleLineComments($(val), show)
852 852 })
853 853
854 854 // since we change the height of the diff container that has anchor points for upper
855 855 // sticky header, we need to tell it to re-calculate those
856 856 if (window.updateSticky !== undefined) {
857 857 // potentially our comments change the active window size, so we
858 858 // notify sticky elements
859 859 updateSticky()
860 860 }
861 861
862 862 return false;
863 863 }
864 864
865 865 this.toggleLineComments = function(node, show) {
866 866
867 867 var trElem = $(node).closest('tr')
868 868
869 869 if (show === true) {
870 870 // mark outdated comments as visible before the toggle;
871 871 $(trElem).find('.comment-outdated').show();
872 872 $(trElem).removeClass('hide-line-comments');
873 873 } else if (show === false) {
874 874 $(trElem).find('.comment-outdated').hide();
875 875 $(trElem).addClass('hide-line-comments');
876 876 } else {
877 877 // mark outdated comments as visible before the toggle;
878 878 $(trElem).find('.comment-outdated').show();
879 879 $(trElem).toggleClass('hide-line-comments');
880 880 }
881 881
882 882 // since we change the height of the diff container that has anchor points for upper
883 883 // sticky header, we need to tell it to re-calculate those
884 884 if (window.updateSticky !== undefined) {
885 885 // potentially our comments change the active window size, so we
886 886 // notify sticky elements
887 887 updateSticky()
888 888 }
889 889
890 890 };
891 891
892 892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
893 893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
894 894 var commitId = templateContext.commit_data.commit_id;
895 895
896 896 var commentForm = new CommentForm(
897 897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
898 898 var cm = commentForm.getCmInstance();
899 899
900 900 if (resolvesCommentId){
901 901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
902 902 }
903 903
904 904 setTimeout(function() {
905 905 // callbacks
906 906 if (cm !== undefined) {
907 907 commentForm.setPlaceholder(placeholderText);
908 908 if (commentForm.isInline()) {
909 909 cm.focus();
910 910 cm.refresh();
911 911 }
912 912 }
913 913 }, 10);
914 914
915 915 // trigger scrolldown to the resolve comment, since it might be away
916 916 // from the clicked
917 917 if (resolvesCommentId){
918 918 var actionNode = $(commentForm.resolvesActionId).offset();
919 919
920 920 setTimeout(function() {
921 921 if (actionNode) {
922 922 $('body, html').animate({scrollTop: actionNode.top}, 10);
923 923 }
924 924 }, 100);
925 925 }
926 926
927 927 // add dropzone support
928 928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
929 929 var renderer = templateContext.visual.default_renderer;
930 930 if (renderer == 'rst') {
931 931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
932 932 if (isRendered){
933 933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
934 934 }
935 935 } else if (renderer == 'markdown') {
936 936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
937 937 if (isRendered){
938 938 attachmentUrl = '!' + attachmentUrl;
939 939 }
940 940 } else {
941 941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
942 942 }
943 943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
944 944
945 945 return false;
946 946 };
947 947
948 948 //see: https://www.dropzonejs.com/#configuration
949 949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
950 950 {'repo_name': templateContext.repo_name,
951 951 'commit_id': templateContext.commit_data.commit_id})
952 952
953 953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
954 954 if (previewTmpl !== undefined){
955 955 var selectLink = $(formElement).find('.pick-attachment').get(0);
956 956 $(formElement).find('.comment-attachment-uploader').dropzone({
957 957 url: storeUrl,
958 958 headers: {"X-CSRF-Token": CSRF_TOKEN},
959 959 paramName: function () {
960 960 return "attachment"
961 961 }, // The name that will be used to transfer the file
962 962 clickable: selectLink,
963 963 parallelUploads: 1,
964 964 maxFiles: 10,
965 965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
966 966 uploadMultiple: false,
967 967 autoProcessQueue: true, // if false queue will not be processed automatically.
968 968 createImageThumbnails: false,
969 969 previewTemplate: previewTmpl.innerHTML,
970 970
971 971 accept: function (file, done) {
972 972 done();
973 973 },
974 974 init: function () {
975 975
976 976 this.on("sending", function (file, xhr, formData) {
977 977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
978 978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
979 979 });
980 980
981 981 this.on("success", function (file, response) {
982 982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
983 983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
984 984
985 985 var isRendered = false;
986 986 var ext = file.name.split('.').pop();
987 987 var imageExts = templateContext.attachment_store.image_ext;
988 988 if (imageExts.indexOf(ext) !== -1){
989 989 isRendered = true;
990 990 }
991 991
992 992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
993 993 });
994 994
995 995 this.on("error", function (file, errorMessage, xhr) {
996 996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
997 997
998 998 var error = null;
999 999
1000 1000 if (xhr !== undefined){
1001 1001 var httpStatus = xhr.status + " " + xhr.statusText;
1002 1002 if (xhr !== undefined && xhr.status >= 500) {
1003 1003 error = httpStatus;
1004 1004 }
1005 1005 }
1006 1006
1007 1007 if (error === null) {
1008 1008 error = errorMessage.error || errorMessage || httpStatus;
1009 1009 }
1010 1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1011 1011
1012 1012 });
1013 1013 }
1014 1014 });
1015 1015 }
1016 1016 return commentForm;
1017 1017 };
1018 1018
1019 1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1020 1020
1021 1021 var tmpl = $('#cb-comment-general-form-template').html();
1022 1022 tmpl = tmpl.format(null, 'general');
1023 1023 var $form = $(tmpl);
1024 1024
1025 1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1026 1026 var curForm = $formPlaceholder.find('form');
1027 1027 if (curForm){
1028 1028 curForm.remove();
1029 1029 }
1030 1030 $formPlaceholder.append($form);
1031 1031
1032 1032 var _form = $($form[0]);
1033 1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1034 1034 var edit = false;
1035 1035 var comment_id = null;
1036 1036 var commentForm = this.createCommentForm(
1037 1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1038 1038 commentForm.initStatusChangeSelector();
1039 1039
1040 1040 return commentForm;
1041 1041 };
1042 1042
1043 1043 this.editComment = function(node, line_no, f_path) {
1044 1044 self.edit = true;
1045 1045 var $node = $(node);
1046 1046 var $td = $node.closest('td');
1047 1047
1048 1048 var $comment = $(node).closest('.comment');
1049 1049 var comment_id = $($comment).data('commentId');
1050 1050 var isDraft = $($comment).data('commentDraft');
1051 1051 var $editForm = null
1052 1052
1053 1053 var $comments = $node.closest('div.inline-comments');
1054 1054 var $general_comments = null;
1055 1055
1056 1056 if($comments.length){
1057 1057 // inline comments setup
1058 1058 $editForm = $comments.find('.comment-inline-form');
1059 1059 line_no = self.getLineNumber(node)
1060 1060 }
1061 1061 else{
1062 1062 // general comments setup
1063 1063 $comments = $('#comments');
1064 1064 $editForm = $comments.find('.comment-inline-form');
1065 1065 line_no = $comment[0].id
1066 1066 $('#cb-comment-general-form-placeholder').hide();
1067 1067 }
1068 1068
1069 1069 if ($editForm.length === 0) {
1070 1070
1071 1071 // unhide all comments if they are hidden for a proper REPLY mode
1072 1072 var $filediff = $node.closest('.filediff');
1073 1073 $filediff.removeClass('hide-comments');
1074 1074
1075 1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 1076 if(f_path && line_no) {
1077 1077 $editForm.addClass('comment-inline-form-edit')
1078 1078 }
1079 1079
1080 1080 $comment.after($editForm)
1081 1081
1082 1082 var _form = $($editForm[0]).find('form');
1083 1083 var autocompleteActions = ['as_note',];
1084 1084 var commentForm = this.createCommentForm(
1085 1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1086 1086 this.edit, comment_id);
1087 1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1088 1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1089 1089 commentForm.cm.setValue(old_comment_text);
1090 1090 $comment.hide();
1091 1091 tooltipActivate();
1092 1092
1093 1093 // set a CUSTOM submit handler for inline comment edit action.
1094 1094 commentForm.setHandleFormSubmit(function(o) {
1095 1095 var text = commentForm.cm.getValue();
1096 1096 var commentType = commentForm.getCommentType();
1097 1097
1098 1098 if (text === "") {
1099 1099 return;
1100 1100 }
1101 1101
1102 1102 if (old_comment_text == text) {
1103 1103 SwalNoAnimation.fire({
1104 1104 title: 'Unable to edit comment',
1105 1105 html: _gettext('Comment body was not changed.'),
1106 1106 });
1107 1107 return;
1108 1108 }
1109 1109 var excludeCancelBtn = false;
1110 1110 var submitEvent = true;
1111 1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1112 1112 commentForm.cm.setOption("readOnly", true);
1113 1113
1114 1114 // Read last version known
1115 1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1116 1116 var version = versionSelector.data('lastVersion');
1117 1117
1118 1118 if (!version) {
1119 1119 version = 0;
1120 1120 }
1121 1121
1122 1122 var postData = {
1123 1123 'text': text,
1124 1124 'f_path': f_path,
1125 1125 'line': line_no,
1126 1126 'comment_type': commentType,
1127 1127 'draft': isDraft,
1128 1128 'version': version,
1129 1129 'csrf_token': CSRF_TOKEN
1130 1130 };
1131 1131
1132 1132 var submitSuccessCallback = function(json_data) {
1133 1133 $editForm.remove();
1134 1134 $comment.show();
1135 1135 var postData = {
1136 1136 'text': text,
1137 1137 'renderer': $comment.attr('data-comment-renderer'),
1138 1138 'csrf_token': CSRF_TOKEN
1139 1139 };
1140 1140
1141 1141 /* Inject new edited version selector */
1142 1142 var updateCommentVersionDropDown = function () {
1143 1143 var versionSelectId = '#comment_versions_'+comment_id;
1144 1144 var preLoadVersionData = [
1145 1145 {
1146 1146 id: json_data['comment_version'],
1147 1147 text: "v{0}".format(json_data['comment_version']),
1148 1148 action: function () {
1149 1149 Rhodecode.comments.showVersion(
1150 1150 json_data['comment_id'],
1151 1151 json_data['comment_history_id']
1152 1152 )
1153 1153 },
1154 1154 comment_version: json_data['comment_version'],
1155 1155 comment_author_username: json_data['comment_author_username'],
1156 1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1157 1157 comment_created_on: json_data['comment_created_on'],
1158 1158 },
1159 1159 ]
1160 1160
1161 1161
1162 1162 if ($(versionSelectId).data('select2')) {
1163 1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1164 1164 $(versionSelectId).select2("destroy");
1165 1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1166 1166 }
1167 1167
1168 1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1169 1169
1170 1170 $comment.attr('data-comment-text', utf8ToB64(text));
1171 1171
1172 1172 var versionSelector = $('#comment_versions_'+comment_id);
1173 1173
1174 1174 // set lastVersion so we know our last edit version
1175 1175 versionSelector.data('lastVersion', json_data['comment_version'])
1176 1176 versionSelector.parent().show();
1177 1177 }
1178 1178 updateCommentVersionDropDown();
1179 1179
1180 1180 // by default we reset state of comment preserving the text
1181 1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1182 1182 var prefix = "Error while editing this comment.\n"
1183 1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1184 1184 ajaxErrorSwal(message);
1185 1185 };
1186 1186
1187 1187 var successRenderCommit = function(o){
1188 1188 $comment.show();
1189 1189 $comment[0].lastElementChild.innerHTML = o;
1190 1190 };
1191 1191
1192 1192 var previewUrl = pyroutes.url(
1193 1193 'repo_commit_comment_preview',
1194 1194 {'repo_name': templateContext.repo_name,
1195 1195 'commit_id': templateContext.commit_data.commit_id});
1196 1196
1197 1197 _submitAjaxPOST(
1198 1198 previewUrl, postData, successRenderCommit, failRenderCommit
1199 1199 );
1200 1200
1201 1201 try {
1202 1202 var html = json_data.rendered_text;
1203 1203 var lineno = json_data.line_no;
1204 1204 var target_id = json_data.target_id;
1205 1205
1206 1206 $comments.find('.cb-comment-add-button').before(html);
1207 1207
1208 1208 // run global callback on submit
1209 1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1210 1210
1211 1211 } catch (e) {
1212 1212 console.error(e);
1213 1213 }
1214 1214
1215 1215 // re trigger the linkification of next/prev navigation
1216 1216 linkifyComments($('.inline-comment-injected'));
1217 1217 timeagoActivate();
1218 1218 tooltipActivate();
1219 1219
1220 1220 if (window.updateSticky !== undefined) {
1221 1221 // potentially our comments change the active window size, so we
1222 1222 // notify sticky elements
1223 1223 updateSticky()
1224 1224 }
1225 1225
1226 1226 if (window.refreshAllComments !== undefined && !isDraft) {
1227 1227 // if we have this handler, run it, and refresh all comments boxes
1228 1228 refreshAllComments()
1229 1229 }
1230 1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 1231 // if we have this handler, run it, and refresh all comments boxes
1232 1232 refreshDraftComments();
1233 1233 }
1234 1234
1235 1235 commentForm.setActionButtonsDisabled(false);
1236 1236
1237 1237 };
1238 1238
1239 1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1240 1240 var prefix = "Error while editing comment.\n"
1241 1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1242 1242 if (jqXHR.status == 409){
1243 1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1244 1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1245 1245 } else {
1246 1246 ajaxErrorSwal(message);
1247 1247 }
1248 1248
1249 1249 commentForm.resetCommentFormState(text)
1250 1250 };
1251 1251 commentForm.submitAjaxPOST(
1252 1252 commentForm.submitUrl, postData,
1253 1253 submitSuccessCallback,
1254 1254 submitFailCallback);
1255 1255 });
1256 1256 }
1257 1257
1258 1258 $editForm.addClass('comment-inline-form-open');
1259 1259 };
1260 1260
1261 1261 this.attachComment = function(json_data) {
1262 1262 var self = this;
1263 1263 $.each(json_data, function(idx, val) {
1264 1264 var json_data_elem = [val]
1265 1265 var isInline = val.comment_f_path && val.comment_lineno
1266 1266
1267 1267 if (isInline) {
1268 1268 self.attachInlineComment(json_data_elem)
1269 1269 } else {
1270 1270 self.attachGeneralComment(json_data_elem)
1271 1271 }
1272 1272 })
1273 1273
1274 1274 }
1275 1275
1276 1276 this.attachGeneralComment = function(json_data) {
1277 1277 $.each(json_data, function(idx, val) {
1278 1278 $('#injected_page_comments').append(val.rendered_text);
1279 1279 })
1280 1280 }
1281 1281
1282 1282 this.attachInlineComment = function(json_data) {
1283 1283
1284 1284 $.each(json_data, function (idx, val) {
1285 1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 1286 var html = val.rendered_text;
1287 1287 var $inlineComments = $('#' + val.target_id)
1288 1288 .find(line_qry)
1289 1289 .find('.inline-comments');
1290 1290
1291 1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292 1292
1293 1293 if (lastComment.length === 0) {
1294 1294 // first comment, we append simply
1295 1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 1296 } else {
1297 1297 $(lastComment).after(html)
1298 1298 }
1299 1299
1300 1300 })
1301 1301
1302 1302 };
1303 1303
1304 1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 1305 // create a new reply HTML form from template
1306 1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 1308 return $(tmpl);
1309 1309 }
1310 1310
1311 1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 1312 self.edit = false;
1313 1313 var $node = $(node);
1314 1314 var $td = $node.closest('td');
1315 1315 var resolvesCommentId = resolutionComment || null;
1316 1316
1317 1317 var $replyForm = $td.find('.comment-inline-form');
1318 1318
1319 1319 // if form isn't existing, we're generating a new one and injecting it.
1320 1320 if ($replyForm.length === 0) {
1321 1321
1322 1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 1323 self.toggleLineComments($node, true);
1324 1324
1325 1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1326 1326
1327 1327 var $comments = $td.find('.inline-comments');
1328 1328
1329 1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 1330 if ($comments.length===0) {
1331 1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 1334 $td.append($($reply_container).html());
1335 1335 }
1336 1336
1337 1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 1339 // set marker, that we have a open form
1340 1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 1341 $replyWrapper.addClass('comment-form-active');
1342 1342
1343 1343 var lastComment = $comments.find('.comment-inline').last();
1344 1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 1345 $replyWrapper.show();
1346 1346 }
1347 1347
1348 1348 var _form = $($replyForm[0]).find('form');
1349 1349 var autocompleteActions = ['as_note', 'as_todo'];
1350 1350 var comment_id=null;
1351 1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 1352 var commentForm = self.createCommentForm(
1353 1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 1354 self.edit, comment_id);
1355 1355
1356 1356 // set a CUSTOM submit handler for inline comments.
1357 1357 commentForm.setHandleFormSubmit(function(o) {
1358 1358 var text = commentForm.cm.getValue();
1359 1359 var commentType = commentForm.getCommentType();
1360 1360 var resolvesCommentId = commentForm.getResolvesId();
1361 1361 var isDraft = commentForm.getDraftState();
1362 1362
1363 1363 if (text === "") {
1364 1364 return;
1365 1365 }
1366 1366
1367 1367 if (line_no === undefined) {
1368 1368 alert('Error: unable to fetch line number for this inline comment !');
1369 1369 return;
1370 1370 }
1371 1371
1372 1372 if (f_path === undefined) {
1373 1373 alert('Error: unable to fetch file path for this inline comment !');
1374 1374 return;
1375 1375 }
1376 1376
1377 1377 var excludeCancelBtn = false;
1378 1378 var submitEvent = true;
1379 1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1380 1380 commentForm.cm.setOption("readOnly", true);
1381 1381 var postData = {
1382 1382 'text': text,
1383 1383 'f_path': f_path,
1384 1384 'line': line_no,
1385 1385 'comment_type': commentType,
1386 1386 'draft': isDraft,
1387 1387 'csrf_token': CSRF_TOKEN
1388 1388 };
1389 1389 if (resolvesCommentId){
1390 1390 postData['resolves_comment_id'] = resolvesCommentId;
1391 1391 }
1392 1392
1393 1393 // submitSuccess for inline commits
1394 1394 var submitSuccessCallback = function(json_data) {
1395 1395
1396 1396 $replyForm.remove();
1397 1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398 1398
1399 1399 try {
1400 1400
1401 1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 1402 self.attachInlineComment(json_data)
1403 1403
1404 1404 //mark visually which comment was resolved
1405 1405 if (resolvesCommentId) {
1406 1406 commentForm.markCommentResolved(resolvesCommentId);
1407 1407 }
1408 1408
1409 1409 // run global callback on submit
1410 1410 commentForm.globalSubmitSuccessCallback({
1411 1411 draft: isDraft,
1412 1412 comment_id: comment_id
1413 1413 });
1414 1414
1415 1415 } catch (e) {
1416 1416 console.error(e);
1417 1417 }
1418 1418
1419 1419 if (window.updateSticky !== undefined) {
1420 1420 // potentially our comments change the active window size, so we
1421 1421 // notify sticky elements
1422 1422 updateSticky()
1423 1423 }
1424 1424
1425 1425 if (window.refreshAllComments !== undefined && !isDraft) {
1426 1426 // if we have this handler, run it, and refresh all comments boxes
1427 1427 refreshAllComments()
1428 1428 }
1429 1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 1430 // if we have this handler, run it, and refresh all comments boxes
1431 1431 refreshDraftComments();
1432 1432 }
1433 1433
1434 1434 commentForm.setActionButtonsDisabled(false);
1435 1435
1436 1436 // re trigger the linkification of next/prev navigation
1437 1437 linkifyComments($('.inline-comment-injected'));
1438 1438 timeagoActivate();
1439 1439 tooltipActivate();
1440 1440 };
1441 1441
1442 1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1443 1443 var prefix = "Error while submitting comment.\n"
1444 1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1445 1445 ajaxErrorSwal(message);
1446 1446 commentForm.resetCommentFormState(text)
1447 1447 };
1448 1448
1449 1449 commentForm.submitAjaxPOST(
1450 1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1451 1451 });
1452 1452 }
1453 1453
1454 1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 1455 $replyForm.addClass('comment-inline-form-open');
1456 1456 tooltipActivate();
1457 1457 };
1458 1458
1459 1459 this.createResolutionComment = function(commentId){
1460 1460 // hide the trigger text
1461 1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1462 1462
1463 1463 var comment = $('#comment-'+commentId);
1464 1464 var commentData = comment.data();
1465 console.log(commentData);
1466 1465
1467 1466 if (commentData.commentInline) {
1468 1467 var f_path = commentData.commentFPath;
1469 1468 var line_no = commentData.commentLineNo;
1470 1469 this.createComment(comment, f_path, line_no, commentId)
1471 1470 } else {
1472 1471 this.createGeneralComment('general', "$placeholder", commentId)
1473 1472 }
1474 1473
1475 1474 return false;
1476 1475 };
1477 1476
1478 1477 this.submitResolution = function(commentId){
1479 1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1480 1479 var commentForm = form.get(0).CommentForm;
1481 1480
1482 1481 var cm = commentForm.getCmInstance();
1483 1482 var renderer = templateContext.visual.default_renderer;
1484 1483 if (renderer == 'rst'){
1485 1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1486 1485 } else if (renderer == 'markdown') {
1487 1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1488 1487 } else {
1489 1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1490 1489 }
1491 1490
1492 1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1493 1492 form.submit();
1494 1493 return false;
1495 1494 };
1496 1495
1497 1496 };
1498 1497
1499 1498 window.commentHelp = function(renderer) {
1500 1499 var funcData = {'renderer': renderer}
1501 1500 return renderTemplate('commentHelpHovercard', funcData)
1502 1501 } No newline at end of file
@@ -1,206 +1,205 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 10 'repo_name': repo_name,
11 11 'status': status_change,
12 12 'comment_file': comment_file,
13 13 'comment_line': comment_line,
14 14 'comment_type': comment_type,
15 15 'comment_id': comment_id,
16 16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title_safe,
18 18 'pr_id': pull_request.pull_request_id,
19 19 'mention_prefix': '[mention] ' if mention else '',
20 20 }
21 21
22 22 if comment_file:
23 23 subject_template = email_pr_comment_file_subject_template or \
24 24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 25 else:
26 26 if status_change:
27 27 subject_template = email_pr_comment_status_change_subject_template or \
28 28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 29 else:
30 30 subject_template = email_pr_comment_subject_template or \
31 31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
32 32 %>
33 33
34
35 34 ${subject_template.format(**data) |n}
36 35 </%def>
37 36
38 37 ## PLAINTEXT VERSION OF BODY
39 38 <%def name="body_plaintext()" filter="n,trim">
40 39 <%
41 40 data = {
42 41 'user': h.person(user),
43 42 'repo_name': repo_name,
44 43 'status': status_change,
45 44 'comment_file': comment_file,
46 45 'comment_line': comment_line,
47 46 'comment_type': comment_type,
48 47 'comment_id': comment_id,
49 48
50 'pr_title': pull_request.title,
49 'pr_title': pull_request.title_safe,
51 50 'pr_id': pull_request.pull_request_id,
52 51 'source_ref_type': pull_request.source_ref_parts.type,
53 52 'source_ref_name': pull_request.source_ref_parts.name,
54 53 'target_ref_type': pull_request.target_ref_parts.type,
55 54 'target_ref_name': pull_request.target_ref_parts.name,
56 55 'source_repo': pull_request_source_repo.repo_name,
57 56 'target_repo': pull_request_target_repo.repo_name,
58 57 'source_repo_url': pull_request_source_repo_url,
59 58 'target_repo_url': pull_request_target_repo_url,
60 59 }
61 60 %>
62 61
63 62 * ${_('Comment link')}: ${pr_comment_url}
64 63
65 64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
66 65
67 66 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
68 67
69 68 %if status_change and not closing_pr:
70 69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
71 70
72 71 %elif status_change and closing_pr:
73 72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
74 73
75 74 %endif
76 75 %if comment_file:
77 76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
78 77
79 78 %endif
80 79 % if comment_type == 'todo':
81 80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
82 81 % else:
83 82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
84 83 % endif
85 84
86 85 ${comment_body |n, trim}
87 86
88 87 ---
89 88 ${self.plaintext_footer()}
90 89 </%def>
91 90
92 91
93 92 <%
94 93 data = {
95 94 'user': h.person(user),
96 95 'comment_file': comment_file,
97 96 'comment_line': comment_line,
98 97 'comment_type': comment_type,
99 98 'comment_id': comment_id,
100 99 'renderer_type': renderer_type or 'plain',
101 100
102 'pr_title': pull_request.title,
101 'pr_title': pull_request.title_safe,
103 102 'pr_id': pull_request.pull_request_id,
104 103 'status': status_change,
105 104 'source_ref_type': pull_request.source_ref_parts.type,
106 105 'source_ref_name': pull_request.source_ref_parts.name,
107 106 'target_ref_type': pull_request.target_ref_parts.type,
108 107 'target_ref_name': pull_request.target_ref_parts.name,
109 108 'source_repo': pull_request_source_repo.repo_name,
110 109 'target_repo': pull_request_target_repo.repo_name,
111 110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
112 111 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
113 112 }
114 113 %>
115 114
116 115 ## header
117 116 <table style="text-align:left;vertical-align:middle;width: 100%">
118 117 <tr>
119 118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
120 119
121 120 <div style="margin: 0; font-weight: bold">
122 121 <div class="clear-both" style="margin-bottom: 4px">
123 122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
124 123 ${_('left a')}
125 124 <a href="${pr_comment_url}" style="${base.link_css()}">
126 125 % if comment_file:
127 126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
128 127 % else:
129 128 ${_('{comment_type} on pull request.').format(**data) |n}
130 129 % endif
131 130 </a>
132 131 </div>
133 132 <div style="margin-top: 10px"></div>
134 133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
135 134 </div>
136 135
137 136 </td>
138 137 </tr>
139 138
140 139 </table>
141 140 <div class="clear-both"></div>
142 141 ## main body
143 142 <table style="text-align:left;vertical-align:middle;width: 100%">
144 143
145 144 ## spacing def
146 145 <tr>
147 146 <td style="width: 130px"></td>
148 147 <td></td>
149 148 </tr>
150 149
151 150 % if status_change:
152 151 <tr>
153 152 <td style="padding-right:20px;">${_('Review Status')}:</td>
154 153 <td>
155 154 % if closing_pr:
156 155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 156 % else:
158 157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
159 158 % endif
160 159 </td>
161 160 </tr>
162 161 % endif
163 162 <tr>
164 163 <td style="padding-right:20px;">${_('Pull request')}:</td>
165 164 <td>
166 165 <a href="${pull_request_url}" style="${base.link_css()}">
167 166 !${pull_request.pull_request_id}
168 167 </a>
169 168 </td>
170 169 </tr>
171 170
172 171 <tr>
173 172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
174 173 <td style="line-height:20px;">
175 174 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
176 175 &rarr;
177 176 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
178 177 </td>
179 178 </tr>
180 179
181 180 % if comment_file:
182 181 <tr>
183 182 <td style="padding-right:20px;">${_('File')}:</td>
184 183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
185 184 </tr>
186 185 % endif
187 186
188 187 <tr style="border-bottom:1px solid #dbd9da;">
189 188 <td colspan="2" style="padding-right:20px;">
190 189 % if comment_type == 'todo':
191 190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
192 191 % else:
193 192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
194 193 % endif
195 194 </td>
196 195 </tr>
197 196
198 197 <tr>
199 198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
200 199 </tr>
201 200
202 201 <tr>
203 202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
204 203 <td></td>
205 204 </tr>
206 205 </table>
@@ -1,154 +1,154 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 12 }
13 13
14 14 if user_role == 'observer':
15 15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 16 else:
17 17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
18 18 %>
19 19
20 20 ${subject_template.format(**data) |n}
21 21 </%def>
22 22
23 23 ## PLAINTEXT VERSION OF BODY
24 24 <%def name="body_plaintext()" filter="n,trim">
25 25 <%
26 26 data = {
27 27 'user': h.person(user),
28 28 'pr_id': pull_request.pull_request_id,
29 'pr_title': pull_request.title,
29 'pr_title': pull_request.title_safe,
30 30 'source_ref_type': pull_request.source_ref_parts.type,
31 31 'source_ref_name': pull_request.source_ref_parts.name,
32 32 'target_ref_type': pull_request.target_ref_parts.type,
33 33 'target_ref_name': pull_request.target_ref_parts.name,
34 34 'repo_url': pull_request_source_repo_url,
35 35 'source_repo': pull_request_source_repo.repo_name,
36 36 'target_repo': pull_request_target_repo.repo_name,
37 37 'source_repo_url': pull_request_source_repo_url,
38 38 'target_repo_url': pull_request_target_repo_url,
39 39 }
40 40
41 41 %>
42 42
43 43 * ${_('Pull Request link')}: ${pull_request_url}
44 44
45 45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
46 46
47 47 * ${_('Title')}: ${pull_request.title}
48 48
49 49 * ${_('Description')}:
50 50
51 51 ${pull_request.description | trim}
52 52
53 53
54 54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
55 55
56 56 % for commit_id, message in pull_request_commits:
57 57 - ${h.short_id(commit_id)}
58 58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
59 59
60 60 % endfor
61 61
62 62 ---
63 63 ${self.plaintext_footer()}
64 64 </%def>
65 65 <%
66 66 data = {
67 67 'user': h.person(user),
68 68 'pr_id': pull_request.pull_request_id,
69 'pr_title': pull_request.title,
69 'pr_title': pull_request.title_safe,
70 70 'source_ref_type': pull_request.source_ref_parts.type,
71 71 'source_ref_name': pull_request.source_ref_parts.name,
72 72 'target_ref_type': pull_request.target_ref_parts.type,
73 73 'target_ref_name': pull_request.target_ref_parts.name,
74 74 'repo_url': pull_request_source_repo_url,
75 75 'source_repo': pull_request_source_repo.repo_name,
76 76 'target_repo': pull_request_target_repo.repo_name,
77 77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
78 78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
79 79 }
80 80 %>
81 81 ## header
82 82 <table style="text-align:left;vertical-align:middle;width: 100%">
83 83 <tr>
84 84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
85 85 <div style="margin: 0; font-weight: bold">
86 86 % if user_role == 'observer':
87 87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 89 ${_('added you as observer to')}
90 90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 91 </div>
92 92 % else:
93 93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
94 94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
95 95 ${_('requested a')}
96 96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
97 97 </div>
98 98 % endif
99 99 <div style="margin-top: 10px"></div>
100 100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
101 101 </div>
102 102 </td>
103 103 </tr>
104 104
105 105 </table>
106 106 <div class="clear-both"></div>
107 107 ## main body
108 108 <table style="text-align:left;vertical-align:middle;width: 100%">
109 109 ## spacing def
110 110 <tr>
111 111 <td style="width: 130px"></td>
112 112 <td></td>
113 113 </tr>
114 114
115 115 <tr>
116 116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 117 <td>
118 118 <a href="${pull_request_url}" style="${base.link_css()}">
119 119 !${pull_request.pull_request_id}
120 120 </a>
121 121 </td>
122 122 </tr>
123 123
124 124 <tr>
125 125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
126 126 <td style="line-height:20px;">
127 127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
128 128 &rarr;
129 129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
130 130 </td>
131 131 </tr>
132 132
133 133 <tr>
134 134 <td style="padding-right:20px;">${_('Description')}:</td>
135 135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
136 136 </tr>
137 137 <tr>
138 138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
139 139 <td></td>
140 140 </tr>
141 141
142 142 <tr>
143 143 <td colspan="2">
144 144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
145 145 % for commit_id, message in pull_request_commits:
146 146 <li style="margin:0 0 1em;">
147 147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
148 148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
149 149 </li>
150 150 % endfor
151 151 </ol>
152 152 </td>
153 153 </tr>
154 154 </table>
@@ -1,172 +1,172 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'updating_user': '@'+h.person(updating_user),
10 10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 12 }
13 13
14 14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
15 15 %>
16 16
17 17 ${subject_template.format(**data) |n}
18 18 </%def>
19 19
20 20 ## PLAINTEXT VERSION OF BODY
21 21 <%def name="body_plaintext()" filter="n,trim">
22 22 <%
23 23 data = {
24 24 'updating_user': h.person(updating_user),
25 25 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title,
26 'pr_title': pull_request.title_safe,
27 27 'source_ref_type': pull_request.source_ref_parts.type,
28 28 'source_ref_name': pull_request.source_ref_parts.name,
29 29 'target_ref_type': pull_request.target_ref_parts.type,
30 30 'target_ref_name': pull_request.target_ref_parts.name,
31 31 'repo_url': pull_request_source_repo_url,
32 32 'source_repo': pull_request_source_repo.repo_name,
33 33 'target_repo': pull_request_target_repo.repo_name,
34 34 'source_repo_url': pull_request_source_repo_url,
35 35 'target_repo_url': pull_request_target_repo_url,
36 36 }
37 37 %>
38 38
39 39 * ${_('Pull Request link')}: ${pull_request_url}
40 40
41 41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
42 42
43 43 * ${_('Title')}: ${pull_request.title}
44 44
45 45 * ${_('Description')}:
46 46
47 47 ${pull_request.description | trim}
48 48
49 49 * Changed commits:
50 50
51 51 - Added: ${len(added_commits)}
52 52 - Removed: ${len(removed_commits)}
53 53
54 54 * Changed files:
55 55
56 56 %if not changed_files:
57 57 No file changes found
58 58 %else:
59 59 %for file_name in added_files:
60 60 - A `${file_name}`
61 61 %endfor
62 62 %for file_name in modified_files:
63 63 - M `${file_name}`
64 64 %endfor
65 65 %for file_name in removed_files:
66 66 - R `${file_name}`
67 67 %endfor
68 68 %endif
69 69
70 70 ---
71 71 ${self.plaintext_footer()}
72 72 </%def>
73 73 <%
74 74 data = {
75 75 'updating_user': h.person(updating_user),
76 76 'pr_id': pull_request.pull_request_id,
77 'pr_title': pull_request.title,
77 'pr_title': pull_request.title_safe,
78 78 'source_ref_type': pull_request.source_ref_parts.type,
79 79 'source_ref_name': pull_request.source_ref_parts.name,
80 80 'target_ref_type': pull_request.target_ref_parts.type,
81 81 'target_ref_name': pull_request.target_ref_parts.name,
82 82 'repo_url': pull_request_source_repo_url,
83 83 'source_repo': pull_request_source_repo.repo_name,
84 84 'target_repo': pull_request_target_repo.repo_name,
85 85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
86 86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
87 87 }
88 88 %>
89 89
90 90 ## header
91 91 <table style="text-align:left;vertical-align:middle;width: 100%">
92 92 <tr>
93 93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
94 94
95 95 <div style="margin: 0; font-weight: bold">
96 96 <div class="clear-both" style="margin-bottom: 4px">
97 97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
98 98 ${_('updated')}
99 99 <a href="${pull_request_url}" style="${base.link_css()}">
100 100 ${_('pull request.').format(**data) }
101 101 </a>
102 102 </div>
103 103 <div style="margin-top: 10px"></div>
104 104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
105 105 </div>
106 106
107 107 </td>
108 108 </tr>
109 109
110 110 </table>
111 111 <div class="clear-both"></div>
112 112 ## main body
113 113 <table style="text-align:left;vertical-align:middle;width: 100%">
114 114 ## spacing def
115 115 <tr>
116 116 <td style="width: 130px"></td>
117 117 <td></td>
118 118 </tr>
119 119
120 120 <tr>
121 121 <td style="padding-right:20px;">${_('Pull request')}:</td>
122 122 <td>
123 123 <a href="${pull_request_url}" style="${base.link_css()}">
124 124 !${pull_request.pull_request_id}
125 125 </a>
126 126 </td>
127 127 </tr>
128 128
129 129 <tr>
130 130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
131 131 <td style="line-height:20px;">
132 132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
133 133 &rarr;
134 134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
135 135 </td>
136 136 </tr>
137 137
138 138 <tr>
139 139 <td style="padding-right:20px;">${_('Description')}:</td>
140 140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
141 141 </tr>
142 142 <tr>
143 143 <td style="padding-right:20px;">${_('Changes')}:</td>
144 144 <td>
145 145 <strong>Changed commits:</strong>
146 146 <ul class="changes-ul">
147 147 <li>- Added: ${len(added_commits)}</li>
148 148 <li>- Removed: ${len(removed_commits)}</li>
149 149 </ul>
150 150
151 151 <strong>Changed files:</strong>
152 152 <ul class="changes-ul">
153 153
154 154 %if not changed_files:
155 155 <li>No file changes found</li>
156 156 %else:
157 157 %for file_name in added_files:
158 158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
159 159 %endfor
160 160 %for file_name in modified_files:
161 161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
162 162 %endfor
163 163 %for file_name in removed_files:
164 164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
165 165 %endfor
166 166 %endif
167 167
168 168 </ul>
169 169 </td>
170 170 </tr>
171 171
172 172 </table>
@@ -1,195 +1,197 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 pytest
22 22 import collections
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 26 from rhodecode.model.db import User, PullRequestReviewers
27 27 from rhodecode.model.notification import EmailNotificationModel
28 28
29 29
30 @pytest.fixture()
31 def pr():
32 def factory(ref):
33 return collections.namedtuple(
34 'PullRequest',
35 'pull_request_id, title, title_safe, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')\
36 (200, 'Example Pull Request', 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
37 return factory
38
39
30 40 def test_get_template_obj(app, request_stub):
31 41 template = EmailNotificationModel().get_renderer(
32 42 EmailNotificationModel.TYPE_TEST, request_stub)
33 43 assert isinstance(template, PyramidPartialRenderer)
34 44
35 45
36 46 def test_render_email(app, http_host_only_stub):
37 47 kwargs = {}
38 48 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 49 EmailNotificationModel.TYPE_TEST, **kwargs)
40 50
41 51 # subject
42 52 assert subject == 'Test "Subject" hello "world"'
43 53
44 54 # body plaintext
45 55 assert body_plaintext == 'Email Plaintext Body'
46 56
47 57 # body
48 58 notification_footer1 = 'This is a notification from RhodeCode.'
49 59 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
50 60 assert notification_footer1 in body
51 61 assert notification_footer2 in body
52 62 assert 'Email Body' in body
53 63
54 64
55 65 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
66 def test_render_pr_email(app, user_admin, role, pr):
57 67 ref = collections.namedtuple(
58 68 'Ref', 'name, type')('fxies123', 'book')
59
60 pr = collections.namedtuple('PullRequest',
61 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
62 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
63
69 pr = pr(ref)
64 70 source_repo = target_repo = collections.namedtuple(
65 71 'Repo', 'type, repo_name')('hg', 'pull_request_1')
66 72
67 73 kwargs = {
68 74 'user': User.get_first_super_admin(),
69 75 'pull_request': pr,
70 76 'pull_request_commits': [],
71 77
72 78 'pull_request_target_repo': target_repo,
73 79 'pull_request_target_repo_url': 'x',
74 80
75 81 'pull_request_source_repo': source_repo,
76 82 'pull_request_source_repo_url': 'x',
77 83
78 84 'pull_request_url': 'http://localhost/pr1',
79 85 'user_role': role,
80 86 }
81 87
82 88 subject, body, body_plaintext = EmailNotificationModel().render_email(
83 89 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
84 90
85 91 # subject
86 92 if role == PullRequestReviewers.ROLE_REVIEWER:
87 93 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 94 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 95 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
90 96
91 97
92 def test_render_pr_update_email(app, user_admin):
98 def test_render_pr_update_email(app, user_admin, pr):
93 99 ref = collections.namedtuple(
94 100 'Ref', 'name, type')('fxies123', 'book')
95 101
96 pr = collections.namedtuple('PullRequest',
97 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
98 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
102 pr = pr(ref)
99 103
100 104 source_repo = target_repo = collections.namedtuple(
101 105 'Repo', 'type, repo_name')('hg', 'pull_request_1')
102 106
103 107 commit_changes = AttributeDict({
104 108 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
105 109 'removed': ['eeeeeeeeeee'],
106 110 })
107 111 file_changes = AttributeDict({
108 112 'added': ['a/file1.md', 'file2.py'],
109 113 'modified': ['b/modified_file.rst'],
110 114 'removed': ['.idea'],
111 115 })
112 116
113 117 kwargs = {
114 118 'updating_user': User.get_first_super_admin(),
115 119
116 120 'pull_request': pr,
117 121 'pull_request_commits': [],
118 122
119 123 'pull_request_target_repo': target_repo,
120 124 'pull_request_target_repo_url': 'x',
121 125
122 126 'pull_request_source_repo': source_repo,
123 127 'pull_request_source_repo_url': 'x',
124 128
125 129 'pull_request_url': 'http://localhost/pr1',
126 130
127 131 'pr_comment_url': 'http://comment-url',
128 132 'pr_comment_reply_url': 'http://comment-url#reply',
129 133 'ancestor_commit_id': 'f39bd443',
130 134 'added_commits': commit_changes.added,
131 135 'removed_commits': commit_changes.removed,
132 136 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
133 137 'added_files': file_changes.added,
134 138 'modified_files': file_changes.modified,
135 139 'removed_files': file_changes.removed,
136 140 }
137 141
138 142 subject, body, body_plaintext = EmailNotificationModel().render_email(
139 143 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
140 144
141 145 # subject
142 146 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
143 147
144 148
145 149 @pytest.mark.parametrize('mention', [
146 150 True,
147 151 False
148 152 ])
149 153 @pytest.mark.parametrize('email_type', [
150 154 EmailNotificationModel.TYPE_COMMIT_COMMENT,
151 155 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
152 156 ])
153 def test_render_comment_subject_no_newlines(app, mention, email_type):
157 def test_render_comment_subject_no_newlines(app, mention, email_type, pr):
154 158 ref = collections.namedtuple(
155 159 'Ref', 'name, type')('fxies123', 'book')
156 160
157 pr = collections.namedtuple('PullRequest',
158 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
159 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
161 pr = pr(ref)
160 162
161 163 source_repo = target_repo = collections.namedtuple(
162 164 'Repo', 'type, repo_name')('hg', 'pull_request_1')
163 165
164 166 kwargs = {
165 167 'user': User.get_first_super_admin(),
166 168 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
167 169 'status_change': 'approved',
168 170 'commit_target_repo_url': 'http://foo.example.com/#comment1',
169 171 'repo_name': 'test-repo',
170 172 'comment_file': 'test-file.py',
171 173 'comment_line': 'n100',
172 174 'comment_type': 'note',
173 175 'comment_id': 2048,
174 176 'commit_comment_url': 'http://comment-url',
175 177 'commit_comment_reply_url': 'http://comment-url/#Reply',
176 178 'instance_url': 'http://rc-instance',
177 179 'comment_body': 'hello world',
178 180 'mention': mention,
179 181
180 182 'pr_comment_url': 'http://comment-url',
181 183 'pr_comment_reply_url': 'http://comment-url/#Reply',
182 184 'pull_request': pr,
183 185 'pull_request_commits': [],
184 186
185 187 'pull_request_target_repo': target_repo,
186 188 'pull_request_target_repo_url': 'x',
187 189
188 190 'pull_request_source_repo': source_repo,
189 191 'pull_request_source_repo_url': 'x',
190 192
191 193 'pull_request_url': 'http://code.rc.com/_pr/123'
192 194 }
193 195 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
194 196
195 197 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now