##// END OF EJS Templates
git: use force fetch and update for target ref. This solves a case...
marcink -
r2784:e8c62649 default
parent child Browse files
Show More
@@ -1,1140 +1,1203 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 35 from rhodecode.tests.utils import AssertResponse
36 36
37 37
38 38 def route_path(name, params=None, **kwargs):
39 39 import urllib
40 40
41 41 base_url = {
42 42 'repo_changelog': '/{repo_name}/changelog',
43 43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 56 }[name].format(**kwargs)
57 57
58 58 if params:
59 59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 60 return base_url
61 61
62 62
63 63 @pytest.mark.usefixtures('app', 'autologin_user')
64 64 @pytest.mark.backends("git", "hg")
65 65 class TestPullrequestsView(object):
66 66
67 67 def test_index(self, backend):
68 68 self.app.get(route_path(
69 69 'pullrequest_new',
70 70 repo_name=backend.repo_name))
71 71
72 72 def test_option_menu_create_pull_request_exists(self, backend):
73 73 repo_name = backend.repo_name
74 74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75 75
76 76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 77 'pullrequest_new', repo_name=repo_name)
78 78 response.mustcontain(create_pr_link)
79 79
80 80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 81 repo = backend.repo
82 82
83 83 self.app.get(
84 84 route_path('pullrequest_new',
85 85 repo_name=repo.repo_name,
86 86 commit=repo.get_commit().raw_id),
87 87 status=200)
88 88
89 89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 90 def test_show(self, pr_util, pr_merge_enabled):
91 91 pull_request = pr_util.create_pull_request(
92 92 mergeable=pr_merge_enabled, enable_notifications=False)
93 93
94 94 response = self.app.get(route_path(
95 95 'pullrequest_show',
96 96 repo_name=pull_request.target_repo.scm_instance().name,
97 97 pull_request_id=pull_request.pull_request_id))
98 98
99 99 for commit_id in pull_request.revisions:
100 100 response.mustcontain(commit_id)
101 101
102 102 assert pull_request.target_ref_parts.type in response
103 103 assert pull_request.target_ref_parts.name in response
104 104 target_clone_url = pull_request.target_repo.clone_url()
105 105 assert target_clone_url in response
106 106
107 107 assert 'class="pull-request-merge"' in response
108 108 assert (
109 109 'Server-side pull request merging is disabled.'
110 110 in response) != pr_merge_enabled
111 111
112 112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
113 113 # Logout
114 114 response = self.app.post(
115 115 h.route_path('logout'),
116 116 params={'csrf_token': csrf_token})
117 117 # Login as regular user
118 118 response = self.app.post(h.route_path('login'),
119 119 {'username': TEST_USER_REGULAR_LOGIN,
120 120 'password': 'test12'})
121 121
122 122 pull_request = pr_util.create_pull_request(
123 123 author=TEST_USER_REGULAR_LOGIN)
124 124
125 125 response = self.app.get(route_path(
126 126 'pullrequest_show',
127 127 repo_name=pull_request.target_repo.scm_instance().name,
128 128 pull_request_id=pull_request.pull_request_id))
129 129
130 130 response.mustcontain('Server-side pull request merging is disabled.')
131 131
132 132 assert_response = response.assert_response()
133 133 # for regular user without a merge permissions, we don't see it
134 134 assert_response.no_element_exists('#close-pull-request-action')
135 135
136 136 user_util.grant_user_permission_to_repo(
137 137 pull_request.target_repo,
138 138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
139 139 'repository.write')
140 140 response = self.app.get(route_path(
141 141 'pullrequest_show',
142 142 repo_name=pull_request.target_repo.scm_instance().name,
143 143 pull_request_id=pull_request.pull_request_id))
144 144
145 145 response.mustcontain('Server-side pull request merging is disabled.')
146 146
147 147 assert_response = response.assert_response()
148 148 # now regular user has a merge permissions, we have CLOSE button
149 149 assert_response.one_element_exists('#close-pull-request-action')
150 150
151 151 def test_show_invalid_commit_id(self, pr_util):
152 152 # Simulating invalid revisions which will cause a lookup error
153 153 pull_request = pr_util.create_pull_request()
154 154 pull_request.revisions = ['invalid']
155 155 Session().add(pull_request)
156 156 Session().commit()
157 157
158 158 response = self.app.get(route_path(
159 159 'pullrequest_show',
160 160 repo_name=pull_request.target_repo.scm_instance().name,
161 161 pull_request_id=pull_request.pull_request_id))
162 162
163 163 for commit_id in pull_request.revisions:
164 164 response.mustcontain(commit_id)
165 165
166 166 def test_show_invalid_source_reference(self, pr_util):
167 167 pull_request = pr_util.create_pull_request()
168 168 pull_request.source_ref = 'branch:b:invalid'
169 169 Session().add(pull_request)
170 170 Session().commit()
171 171
172 172 self.app.get(route_path(
173 173 'pullrequest_show',
174 174 repo_name=pull_request.target_repo.scm_instance().name,
175 175 pull_request_id=pull_request.pull_request_id))
176 176
177 177 def test_edit_title_description(self, pr_util, csrf_token):
178 178 pull_request = pr_util.create_pull_request()
179 179 pull_request_id = pull_request.pull_request_id
180 180
181 181 response = self.app.post(
182 182 route_path('pullrequest_update',
183 183 repo_name=pull_request.target_repo.repo_name,
184 184 pull_request_id=pull_request_id),
185 185 params={
186 186 'edit_pull_request': 'true',
187 187 'title': 'New title',
188 188 'description': 'New description',
189 189 'csrf_token': csrf_token})
190 190
191 191 assert_session_flash(
192 192 response, u'Pull request title & description updated.',
193 193 category='success')
194 194
195 195 pull_request = PullRequest.get(pull_request_id)
196 196 assert pull_request.title == 'New title'
197 197 assert pull_request.description == 'New description'
198 198
199 199 def test_edit_title_description_closed(self, pr_util, csrf_token):
200 200 pull_request = pr_util.create_pull_request()
201 201 pull_request_id = pull_request.pull_request_id
202 202 repo_name = pull_request.target_repo.repo_name
203 203 pr_util.close()
204 204
205 205 response = self.app.post(
206 206 route_path('pullrequest_update',
207 207 repo_name=repo_name, pull_request_id=pull_request_id),
208 208 params={
209 209 'edit_pull_request': 'true',
210 210 'title': 'New title',
211 211 'description': 'New description',
212 212 'csrf_token': csrf_token}, status=200)
213 213 assert_session_flash(
214 214 response, u'Cannot update closed pull requests.',
215 215 category='error')
216 216
217 217 def test_update_invalid_source_reference(self, pr_util, csrf_token):
218 218 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
219 219
220 220 pull_request = pr_util.create_pull_request()
221 221 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
222 222 Session().add(pull_request)
223 223 Session().commit()
224 224
225 225 pull_request_id = pull_request.pull_request_id
226 226
227 227 response = self.app.post(
228 228 route_path('pullrequest_update',
229 229 repo_name=pull_request.target_repo.repo_name,
230 230 pull_request_id=pull_request_id),
231 231 params={'update_commits': 'true',
232 232 'csrf_token': csrf_token})
233 233
234 234 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
235 235 UpdateFailureReason.MISSING_SOURCE_REF])
236 236 assert_session_flash(response, expected_msg, category='error')
237 237
238 238 def test_missing_target_reference(self, pr_util, csrf_token):
239 239 from rhodecode.lib.vcs.backends.base import MergeFailureReason
240 240 pull_request = pr_util.create_pull_request(
241 241 approved=True, mergeable=True)
242 242 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
243 243 Session().add(pull_request)
244 244 Session().commit()
245 245
246 246 pull_request_id = pull_request.pull_request_id
247 247 pull_request_url = route_path(
248 248 'pullrequest_show',
249 249 repo_name=pull_request.target_repo.repo_name,
250 250 pull_request_id=pull_request_id)
251 251
252 252 response = self.app.get(pull_request_url)
253 253
254 254 assertr = AssertResponse(response)
255 255 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
256 256 MergeFailureReason.MISSING_TARGET_REF]
257 257 assertr.element_contains(
258 258 'span[data-role="merge-message"]', str(expected_msg))
259 259
260 260 def test_comment_and_close_pull_request_custom_message_approved(
261 261 self, pr_util, csrf_token, xhr_header):
262 262
263 263 pull_request = pr_util.create_pull_request(approved=True)
264 264 pull_request_id = pull_request.pull_request_id
265 265 author = pull_request.user_id
266 266 repo = pull_request.target_repo.repo_id
267 267
268 268 self.app.post(
269 269 route_path('pullrequest_comment_create',
270 270 repo_name=pull_request.target_repo.scm_instance().name,
271 271 pull_request_id=pull_request_id),
272 272 params={
273 273 'close_pull_request': '1',
274 274 'text': 'Closing a PR',
275 275 'csrf_token': csrf_token},
276 276 extra_environ=xhr_header,)
277 277
278 278 journal = UserLog.query()\
279 279 .filter(UserLog.user_id == author)\
280 280 .filter(UserLog.repository_id == repo) \
281 281 .order_by('user_log_id') \
282 282 .all()
283 283 assert journal[-1].action == 'repo.pull_request.close'
284 284
285 285 pull_request = PullRequest.get(pull_request_id)
286 286 assert pull_request.is_closed()
287 287
288 288 status = ChangesetStatusModel().get_status(
289 289 pull_request.source_repo, pull_request=pull_request)
290 290 assert status == ChangesetStatus.STATUS_APPROVED
291 291 comments = ChangesetComment().query() \
292 292 .filter(ChangesetComment.pull_request == pull_request) \
293 293 .order_by(ChangesetComment.comment_id.asc())\
294 294 .all()
295 295 assert comments[-1].text == 'Closing a PR'
296 296
297 297 def test_comment_force_close_pull_request_rejected(
298 298 self, pr_util, csrf_token, xhr_header):
299 299 pull_request = pr_util.create_pull_request()
300 300 pull_request_id = pull_request.pull_request_id
301 301 PullRequestModel().update_reviewers(
302 302 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
303 303 pull_request.author)
304 304 author = pull_request.user_id
305 305 repo = pull_request.target_repo.repo_id
306 306
307 307 self.app.post(
308 308 route_path('pullrequest_comment_create',
309 309 repo_name=pull_request.target_repo.scm_instance().name,
310 310 pull_request_id=pull_request_id),
311 311 params={
312 312 'close_pull_request': '1',
313 313 'csrf_token': csrf_token},
314 314 extra_environ=xhr_header)
315 315
316 316 pull_request = PullRequest.get(pull_request_id)
317 317
318 318 journal = UserLog.query()\
319 319 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
320 320 .order_by('user_log_id') \
321 321 .all()
322 322 assert journal[-1].action == 'repo.pull_request.close'
323 323
324 324 # check only the latest status, not the review status
325 325 status = ChangesetStatusModel().get_status(
326 326 pull_request.source_repo, pull_request=pull_request)
327 327 assert status == ChangesetStatus.STATUS_REJECTED
328 328
329 329 def test_comment_and_close_pull_request(
330 330 self, pr_util, csrf_token, xhr_header):
331 331 pull_request = pr_util.create_pull_request()
332 332 pull_request_id = pull_request.pull_request_id
333 333
334 334 response = self.app.post(
335 335 route_path('pullrequest_comment_create',
336 336 repo_name=pull_request.target_repo.scm_instance().name,
337 337 pull_request_id=pull_request.pull_request_id),
338 338 params={
339 339 'close_pull_request': 'true',
340 340 'csrf_token': csrf_token},
341 341 extra_environ=xhr_header)
342 342
343 343 assert response.json
344 344
345 345 pull_request = PullRequest.get(pull_request_id)
346 346 assert pull_request.is_closed()
347 347
348 348 # check only the latest status, not the review status
349 349 status = ChangesetStatusModel().get_status(
350 350 pull_request.source_repo, pull_request=pull_request)
351 351 assert status == ChangesetStatus.STATUS_REJECTED
352 352
353 353 def test_create_pull_request(self, backend, csrf_token):
354 354 commits = [
355 355 {'message': 'ancestor'},
356 356 {'message': 'change'},
357 357 {'message': 'change2'},
358 358 ]
359 359 commit_ids = backend.create_master_repo(commits)
360 360 target = backend.create_repo(heads=['ancestor'])
361 361 source = backend.create_repo(heads=['change2'])
362 362
363 363 response = self.app.post(
364 364 route_path('pullrequest_create', repo_name=source.repo_name),
365 365 [
366 366 ('source_repo', source.repo_name),
367 367 ('source_ref', 'branch:default:' + commit_ids['change2']),
368 368 ('target_repo', target.repo_name),
369 369 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
370 370 ('common_ancestor', commit_ids['ancestor']),
371 371 ('pullrequest_desc', 'Description'),
372 372 ('pullrequest_title', 'Title'),
373 373 ('__start__', 'review_members:sequence'),
374 374 ('__start__', 'reviewer:mapping'),
375 375 ('user_id', '1'),
376 376 ('__start__', 'reasons:sequence'),
377 377 ('reason', 'Some reason'),
378 378 ('__end__', 'reasons:sequence'),
379 379 ('__start__', 'rules:sequence'),
380 380 ('__end__', 'rules:sequence'),
381 381 ('mandatory', 'False'),
382 382 ('__end__', 'reviewer:mapping'),
383 383 ('__end__', 'review_members:sequence'),
384 384 ('__start__', 'revisions:sequence'),
385 385 ('revisions', commit_ids['change']),
386 386 ('revisions', commit_ids['change2']),
387 387 ('__end__', 'revisions:sequence'),
388 388 ('user', ''),
389 389 ('csrf_token', csrf_token),
390 390 ],
391 391 status=302)
392 392
393 393 location = response.headers['Location']
394 394 pull_request_id = location.rsplit('/', 1)[1]
395 395 assert pull_request_id != 'new'
396 396 pull_request = PullRequest.get(int(pull_request_id))
397 397
398 398 # check that we have now both revisions
399 399 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
400 400 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
401 401 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
402 402 assert pull_request.target_ref == expected_target_ref
403 403
404 404 def test_reviewer_notifications(self, backend, csrf_token):
405 405 # We have to use the app.post for this test so it will create the
406 406 # notifications properly with the new PR
407 407 commits = [
408 408 {'message': 'ancestor',
409 409 'added': [FileNode('file_A', content='content_of_ancestor')]},
410 410 {'message': 'change',
411 411 'added': [FileNode('file_a', content='content_of_change')]},
412 412 {'message': 'change-child'},
413 413 {'message': 'ancestor-child', 'parents': ['ancestor'],
414 414 'added': [
415 415 FileNode('file_B', content='content_of_ancestor_child')]},
416 416 {'message': 'ancestor-child-2'},
417 417 ]
418 418 commit_ids = backend.create_master_repo(commits)
419 419 target = backend.create_repo(heads=['ancestor-child'])
420 420 source = backend.create_repo(heads=['change'])
421 421
422 422 response = self.app.post(
423 423 route_path('pullrequest_create', repo_name=source.repo_name),
424 424 [
425 425 ('source_repo', source.repo_name),
426 426 ('source_ref', 'branch:default:' + commit_ids['change']),
427 427 ('target_repo', target.repo_name),
428 428 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
429 429 ('common_ancestor', commit_ids['ancestor']),
430 430 ('pullrequest_desc', 'Description'),
431 431 ('pullrequest_title', 'Title'),
432 432 ('__start__', 'review_members:sequence'),
433 433 ('__start__', 'reviewer:mapping'),
434 434 ('user_id', '2'),
435 435 ('__start__', 'reasons:sequence'),
436 436 ('reason', 'Some reason'),
437 437 ('__end__', 'reasons:sequence'),
438 438 ('__start__', 'rules:sequence'),
439 439 ('__end__', 'rules:sequence'),
440 440 ('mandatory', 'False'),
441 441 ('__end__', 'reviewer:mapping'),
442 442 ('__end__', 'review_members:sequence'),
443 443 ('__start__', 'revisions:sequence'),
444 444 ('revisions', commit_ids['change']),
445 445 ('__end__', 'revisions:sequence'),
446 446 ('user', ''),
447 447 ('csrf_token', csrf_token),
448 448 ],
449 449 status=302)
450 450
451 451 location = response.headers['Location']
452 452
453 453 pull_request_id = location.rsplit('/', 1)[1]
454 454 assert pull_request_id != 'new'
455 455 pull_request = PullRequest.get(int(pull_request_id))
456 456
457 457 # Check that a notification was made
458 458 notifications = Notification.query()\
459 459 .filter(Notification.created_by == pull_request.author.user_id,
460 460 Notification.type_ == Notification.TYPE_PULL_REQUEST,
461 461 Notification.subject.contains(
462 462 "wants you to review pull request #%s" % pull_request_id))
463 463 assert len(notifications.all()) == 1
464 464
465 465 # Change reviewers and check that a notification was made
466 466 PullRequestModel().update_reviewers(
467 467 pull_request.pull_request_id, [(1, [], False, [])],
468 468 pull_request.author)
469 469 assert len(notifications.all()) == 2
470 470
471 471 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
472 472 csrf_token):
473 473 commits = [
474 474 {'message': 'ancestor',
475 475 'added': [FileNode('file_A', content='content_of_ancestor')]},
476 476 {'message': 'change',
477 477 'added': [FileNode('file_a', content='content_of_change')]},
478 478 {'message': 'change-child'},
479 479 {'message': 'ancestor-child', 'parents': ['ancestor'],
480 480 'added': [
481 481 FileNode('file_B', content='content_of_ancestor_child')]},
482 482 {'message': 'ancestor-child-2'},
483 483 ]
484 484 commit_ids = backend.create_master_repo(commits)
485 485 target = backend.create_repo(heads=['ancestor-child'])
486 486 source = backend.create_repo(heads=['change'])
487 487
488 488 response = self.app.post(
489 489 route_path('pullrequest_create', repo_name=source.repo_name),
490 490 [
491 491 ('source_repo', source.repo_name),
492 492 ('source_ref', 'branch:default:' + commit_ids['change']),
493 493 ('target_repo', target.repo_name),
494 494 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
495 495 ('common_ancestor', commit_ids['ancestor']),
496 496 ('pullrequest_desc', 'Description'),
497 497 ('pullrequest_title', 'Title'),
498 498 ('__start__', 'review_members:sequence'),
499 499 ('__start__', 'reviewer:mapping'),
500 500 ('user_id', '1'),
501 501 ('__start__', 'reasons:sequence'),
502 502 ('reason', 'Some reason'),
503 503 ('__end__', 'reasons:sequence'),
504 504 ('__start__', 'rules:sequence'),
505 505 ('__end__', 'rules:sequence'),
506 506 ('mandatory', 'False'),
507 507 ('__end__', 'reviewer:mapping'),
508 508 ('__end__', 'review_members:sequence'),
509 509 ('__start__', 'revisions:sequence'),
510 510 ('revisions', commit_ids['change']),
511 511 ('__end__', 'revisions:sequence'),
512 512 ('user', ''),
513 513 ('csrf_token', csrf_token),
514 514 ],
515 515 status=302)
516 516
517 517 location = response.headers['Location']
518 518
519 519 pull_request_id = location.rsplit('/', 1)[1]
520 520 assert pull_request_id != 'new'
521 521 pull_request = PullRequest.get(int(pull_request_id))
522 522
523 523 # target_ref has to point to the ancestor's commit_id in order to
524 524 # show the correct diff
525 525 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
526 526 assert pull_request.target_ref == expected_target_ref
527 527
528 528 # Check generated diff contents
529 529 response = response.follow()
530 530 assert 'content_of_ancestor' not in response.body
531 531 assert 'content_of_ancestor-child' not in response.body
532 532 assert 'content_of_change' in response.body
533 533
534 534 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
535 535 # Clear any previous calls to rcextensions
536 536 rhodecode.EXTENSIONS.calls.clear()
537 537
538 538 pull_request = pr_util.create_pull_request(
539 539 approved=True, mergeable=True)
540 540 pull_request_id = pull_request.pull_request_id
541 541 repo_name = pull_request.target_repo.scm_instance().name,
542 542
543 543 response = self.app.post(
544 544 route_path('pullrequest_merge',
545 545 repo_name=str(repo_name[0]),
546 546 pull_request_id=pull_request_id),
547 547 params={'csrf_token': csrf_token}).follow()
548 548
549 549 pull_request = PullRequest.get(pull_request_id)
550 550
551 551 assert response.status_int == 200
552 552 assert pull_request.is_closed()
553 553 assert_pull_request_status(
554 554 pull_request, ChangesetStatus.STATUS_APPROVED)
555 555
556 556 # Check the relevant log entries were added
557 557 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
558 558 actions = [log.action for log in user_logs]
559 559 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
560 560 expected_actions = [
561 561 u'repo.pull_request.close',
562 562 u'repo.pull_request.merge',
563 563 u'repo.pull_request.comment.create'
564 564 ]
565 565 assert actions == expected_actions
566 566
567 567 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
568 568 actions = [log for log in user_logs]
569 569 assert actions[-1].action == 'user.push'
570 570 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
571 571
572 572 # Check post_push rcextension was really executed
573 573 push_calls = rhodecode.EXTENSIONS.calls['post_push']
574 574 assert len(push_calls) == 1
575 575 unused_last_call_args, last_call_kwargs = push_calls[0]
576 576 assert last_call_kwargs['action'] == 'push'
577 577 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
578 578
579 579 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
580 580 pull_request = pr_util.create_pull_request(mergeable=False)
581 581 pull_request_id = pull_request.pull_request_id
582 582 pull_request = PullRequest.get(pull_request_id)
583 583
584 584 response = self.app.post(
585 585 route_path('pullrequest_merge',
586 586 repo_name=pull_request.target_repo.scm_instance().name,
587 587 pull_request_id=pull_request.pull_request_id),
588 588 params={'csrf_token': csrf_token}).follow()
589 589
590 590 assert response.status_int == 200
591 591 response.mustcontain(
592 592 'Merge is not currently possible because of below failed checks.')
593 593 response.mustcontain('Server-side pull request merging is disabled.')
594 594
595 595 @pytest.mark.skip_backends('svn')
596 596 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
597 597 pull_request = pr_util.create_pull_request(mergeable=True)
598 598 pull_request_id = pull_request.pull_request_id
599 599 repo_name = pull_request.target_repo.scm_instance().name
600 600
601 601 response = self.app.post(
602 602 route_path('pullrequest_merge',
603 603 repo_name=repo_name,
604 604 pull_request_id=pull_request_id),
605 605 params={'csrf_token': csrf_token}).follow()
606 606
607 607 assert response.status_int == 200
608 608
609 609 response.mustcontain(
610 610 'Merge is not currently possible because of below failed checks.')
611 611 response.mustcontain('Pull request reviewer approval is pending.')
612 612
613 613 def test_merge_pull_request_renders_failure_reason(
614 614 self, user_regular, csrf_token, pr_util):
615 615 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
616 616 pull_request_id = pull_request.pull_request_id
617 617 repo_name = pull_request.target_repo.scm_instance().name
618 618
619 619 model_patcher = mock.patch.multiple(
620 620 PullRequestModel,
621 621 merge=mock.Mock(return_value=MergeResponse(
622 622 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
623 623 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
624 624
625 625 with model_patcher:
626 626 response = self.app.post(
627 627 route_path('pullrequest_merge',
628 628 repo_name=repo_name,
629 629 pull_request_id=pull_request_id),
630 630 params={'csrf_token': csrf_token}, status=302)
631 631
632 632 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
633 633 MergeFailureReason.PUSH_FAILED])
634 634
635 635 def test_update_source_revision(self, backend, csrf_token):
636 636 commits = [
637 637 {'message': 'ancestor'},
638 638 {'message': 'change'},
639 639 {'message': 'change-2'},
640 640 ]
641 641 commit_ids = backend.create_master_repo(commits)
642 642 target = backend.create_repo(heads=['ancestor'])
643 643 source = backend.create_repo(heads=['change'])
644 644
645 645 # create pr from a in source to A in target
646 646 pull_request = PullRequest()
647 647 pull_request.source_repo = source
648 648 # TODO: johbo: Make sure that we write the source ref this way!
649 649 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
650 650 branch=backend.default_branch_name, commit_id=commit_ids['change'])
651 651 pull_request.target_repo = target
652 652
653 653 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
654 654 branch=backend.default_branch_name,
655 655 commit_id=commit_ids['ancestor'])
656 656 pull_request.revisions = [commit_ids['change']]
657 657 pull_request.title = u"Test"
658 658 pull_request.description = u"Description"
659 659 pull_request.author = UserModel().get_by_username(
660 660 TEST_USER_ADMIN_LOGIN)
661 661 Session().add(pull_request)
662 662 Session().commit()
663 663 pull_request_id = pull_request.pull_request_id
664 664
665 665 # source has ancestor - change - change-2
666 666 backend.pull_heads(source, heads=['change-2'])
667 667
668 668 # update PR
669 669 self.app.post(
670 670 route_path('pullrequest_update',
671 671 repo_name=target.repo_name,
672 672 pull_request_id=pull_request_id),
673 673 params={'update_commits': 'true',
674 674 'csrf_token': csrf_token})
675 675
676 676 # check that we have now both revisions
677 677 pull_request = PullRequest.get(pull_request_id)
678 678 assert pull_request.revisions == [
679 679 commit_ids['change-2'], commit_ids['change']]
680 680
681 681 # TODO: johbo: this should be a test on its own
682 682 response = self.app.get(route_path(
683 683 'pullrequest_new',
684 684 repo_name=target.repo_name))
685 685 assert response.status_int == 200
686 686 assert 'Pull request updated to' in response.body
687 687 assert 'with 1 added, 0 removed commits.' in response.body
688 688
689 689 def test_update_target_revision(self, backend, csrf_token):
690 690 commits = [
691 691 {'message': 'ancestor'},
692 692 {'message': 'change'},
693 693 {'message': 'ancestor-new', 'parents': ['ancestor']},
694 694 {'message': 'change-rebased'},
695 695 ]
696 696 commit_ids = backend.create_master_repo(commits)
697 697 target = backend.create_repo(heads=['ancestor'])
698 698 source = backend.create_repo(heads=['change'])
699 699
700 700 # create pr from a in source to A in target
701 701 pull_request = PullRequest()
702 702 pull_request.source_repo = source
703 703 # TODO: johbo: Make sure that we write the source ref this way!
704 704 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
705 705 branch=backend.default_branch_name, commit_id=commit_ids['change'])
706 706 pull_request.target_repo = target
707 707 # TODO: johbo: Target ref should be branch based, since tip can jump
708 708 # from branch to branch
709 709 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
710 710 branch=backend.default_branch_name,
711 711 commit_id=commit_ids['ancestor'])
712 712 pull_request.revisions = [commit_ids['change']]
713 713 pull_request.title = u"Test"
714 714 pull_request.description = u"Description"
715 715 pull_request.author = UserModel().get_by_username(
716 716 TEST_USER_ADMIN_LOGIN)
717 717 Session().add(pull_request)
718 718 Session().commit()
719 719 pull_request_id = pull_request.pull_request_id
720 720
721 721 # target has ancestor - ancestor-new
722 722 # source has ancestor - ancestor-new - change-rebased
723 723 backend.pull_heads(target, heads=['ancestor-new'])
724 724 backend.pull_heads(source, heads=['change-rebased'])
725 725
726 726 # update PR
727 727 self.app.post(
728 728 route_path('pullrequest_update',
729 729 repo_name=target.repo_name,
730 730 pull_request_id=pull_request_id),
731 731 params={'update_commits': 'true',
732 732 'csrf_token': csrf_token},
733 733 status=200)
734 734
735 735 # check that we have now both revisions
736 736 pull_request = PullRequest.get(pull_request_id)
737 737 assert pull_request.revisions == [commit_ids['change-rebased']]
738 738 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
739 739 branch=backend.default_branch_name,
740 740 commit_id=commit_ids['ancestor-new'])
741 741
742 742 # TODO: johbo: This should be a test on its own
743 743 response = self.app.get(route_path(
744 744 'pullrequest_new',
745 745 repo_name=target.repo_name))
746 746 assert response.status_int == 200
747 747 assert 'Pull request updated to' in response.body
748 748 assert 'with 1 added, 1 removed commits.' in response.body
749 749
750 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
751 backend = backend_git
752 commits = [
753 {'message': 'master-commit-1'},
754 {'message': 'master-commit-2-change-1'},
755 {'message': 'master-commit-3-change-2'},
756
757 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
758 {'message': 'feat-commit-2'},
759 ]
760 commit_ids = backend.create_master_repo(commits)
761 target = backend.create_repo(heads=['master-commit-3-change-2'])
762 source = backend.create_repo(heads=['feat-commit-2'])
763
764 # create pr from a in source to A in target
765 pull_request = PullRequest()
766 pull_request.source_repo = source
767 # TODO: johbo: Make sure that we write the source ref this way!
768 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
769 branch=backend.default_branch_name,
770 commit_id=commit_ids['master-commit-3-change-2'])
771
772 pull_request.target_repo = target
773 # TODO: johbo: Target ref should be branch based, since tip can jump
774 # from branch to branch
775 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
776 branch=backend.default_branch_name,
777 commit_id=commit_ids['feat-commit-2'])
778
779 pull_request.revisions = [
780 commit_ids['feat-commit-1'],
781 commit_ids['feat-commit-2']
782 ]
783 pull_request.title = u"Test"
784 pull_request.description = u"Description"
785 pull_request.author = UserModel().get_by_username(
786 TEST_USER_ADMIN_LOGIN)
787 Session().add(pull_request)
788 Session().commit()
789 pull_request_id = pull_request.pull_request_id
790
791 # PR is created, now we simulate a force-push into target,
792 # that drops a 2 last commits
793 vcsrepo = target.scm_instance()
794 vcsrepo.config.clear_section('hooks')
795 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
796
797 # update PR
798 self.app.post(
799 route_path('pullrequest_update',
800 repo_name=target.repo_name,
801 pull_request_id=pull_request_id),
802 params={'update_commits': 'true',
803 'csrf_token': csrf_token},
804 status=200)
805
806 response = self.app.get(route_path(
807 'pullrequest_new',
808 repo_name=target.repo_name))
809 assert response.status_int == 200
810 response.mustcontain('Pull request updated to')
811 response.mustcontain('with 0 added, 0 removed commits.')
812
750 813 def test_update_of_ancestor_reference(self, backend, csrf_token):
751 814 commits = [
752 815 {'message': 'ancestor'},
753 816 {'message': 'change'},
754 817 {'message': 'change-2'},
755 818 {'message': 'ancestor-new', 'parents': ['ancestor']},
756 819 {'message': 'change-rebased'},
757 820 ]
758 821 commit_ids = backend.create_master_repo(commits)
759 822 target = backend.create_repo(heads=['ancestor'])
760 823 source = backend.create_repo(heads=['change'])
761 824
762 825 # create pr from a in source to A in target
763 826 pull_request = PullRequest()
764 827 pull_request.source_repo = source
765 828 # TODO: johbo: Make sure that we write the source ref this way!
766 829 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
767 830 branch=backend.default_branch_name,
768 831 commit_id=commit_ids['change'])
769 832 pull_request.target_repo = target
770 833 # TODO: johbo: Target ref should be branch based, since tip can jump
771 834 # from branch to branch
772 835 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
773 836 branch=backend.default_branch_name,
774 837 commit_id=commit_ids['ancestor'])
775 838 pull_request.revisions = [commit_ids['change']]
776 839 pull_request.title = u"Test"
777 840 pull_request.description = u"Description"
778 841 pull_request.author = UserModel().get_by_username(
779 842 TEST_USER_ADMIN_LOGIN)
780 843 Session().add(pull_request)
781 844 Session().commit()
782 845 pull_request_id = pull_request.pull_request_id
783 846
784 847 # target has ancestor - ancestor-new
785 848 # source has ancestor - ancestor-new - change-rebased
786 849 backend.pull_heads(target, heads=['ancestor-new'])
787 850 backend.pull_heads(source, heads=['change-rebased'])
788 851
789 852 # update PR
790 853 self.app.post(
791 854 route_path('pullrequest_update',
792 855 repo_name=target.repo_name,
793 856 pull_request_id=pull_request_id),
794 857 params={'update_commits': 'true',
795 858 'csrf_token': csrf_token},
796 859 status=200)
797 860
798 861 # Expect the target reference to be updated correctly
799 862 pull_request = PullRequest.get(pull_request_id)
800 863 assert pull_request.revisions == [commit_ids['change-rebased']]
801 864 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
802 865 branch=backend.default_branch_name,
803 866 commit_id=commit_ids['ancestor-new'])
804 867 assert pull_request.target_ref == expected_target_ref
805 868
806 869 def test_remove_pull_request_branch(self, backend_git, csrf_token):
807 870 branch_name = 'development'
808 871 commits = [
809 872 {'message': 'initial-commit'},
810 873 {'message': 'old-feature'},
811 874 {'message': 'new-feature', 'branch': branch_name},
812 875 ]
813 876 repo = backend_git.create_repo(commits)
814 877 commit_ids = backend_git.commit_ids
815 878
816 879 pull_request = PullRequest()
817 880 pull_request.source_repo = repo
818 881 pull_request.target_repo = repo
819 882 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
820 883 branch=branch_name, commit_id=commit_ids['new-feature'])
821 884 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
822 885 branch=backend_git.default_branch_name,
823 886 commit_id=commit_ids['old-feature'])
824 887 pull_request.revisions = [commit_ids['new-feature']]
825 888 pull_request.title = u"Test"
826 889 pull_request.description = u"Description"
827 890 pull_request.author = UserModel().get_by_username(
828 891 TEST_USER_ADMIN_LOGIN)
829 892 Session().add(pull_request)
830 893 Session().commit()
831 894
832 895 vcs = repo.scm_instance()
833 896 vcs.remove_ref('refs/heads/{}'.format(branch_name))
834 897
835 898 response = self.app.get(route_path(
836 899 'pullrequest_show',
837 900 repo_name=repo.repo_name,
838 901 pull_request_id=pull_request.pull_request_id))
839 902
840 903 assert response.status_int == 200
841 904 assert_response = AssertResponse(response)
842 905 assert_response.element_contains(
843 906 '#changeset_compare_view_content .alert strong',
844 907 'Missing commits')
845 908 assert_response.element_contains(
846 909 '#changeset_compare_view_content .alert',
847 910 'This pull request cannot be displayed, because one or more'
848 911 ' commits no longer exist in the source repository.')
849 912
850 913 def test_strip_commits_from_pull_request(
851 914 self, backend, pr_util, csrf_token):
852 915 commits = [
853 916 {'message': 'initial-commit'},
854 917 {'message': 'old-feature'},
855 918 {'message': 'new-feature', 'parents': ['initial-commit']},
856 919 ]
857 920 pull_request = pr_util.create_pull_request(
858 921 commits, target_head='initial-commit', source_head='new-feature',
859 922 revisions=['new-feature'])
860 923
861 924 vcs = pr_util.source_repository.scm_instance()
862 925 if backend.alias == 'git':
863 926 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
864 927 else:
865 928 vcs.strip(pr_util.commit_ids['new-feature'])
866 929
867 930 response = self.app.get(route_path(
868 931 'pullrequest_show',
869 932 repo_name=pr_util.target_repository.repo_name,
870 933 pull_request_id=pull_request.pull_request_id))
871 934
872 935 assert response.status_int == 200
873 936 assert_response = AssertResponse(response)
874 937 assert_response.element_contains(
875 938 '#changeset_compare_view_content .alert strong',
876 939 'Missing commits')
877 940 assert_response.element_contains(
878 941 '#changeset_compare_view_content .alert',
879 942 'This pull request cannot be displayed, because one or more'
880 943 ' commits no longer exist in the source repository.')
881 944 assert_response.element_contains(
882 945 '#update_commits',
883 946 'Update commits')
884 947
885 948 def test_strip_commits_and_update(
886 949 self, backend, pr_util, csrf_token):
887 950 commits = [
888 951 {'message': 'initial-commit'},
889 952 {'message': 'old-feature'},
890 953 {'message': 'new-feature', 'parents': ['old-feature']},
891 954 ]
892 955 pull_request = pr_util.create_pull_request(
893 956 commits, target_head='old-feature', source_head='new-feature',
894 957 revisions=['new-feature'], mergeable=True)
895 958
896 959 vcs = pr_util.source_repository.scm_instance()
897 960 if backend.alias == 'git':
898 961 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
899 962 else:
900 963 vcs.strip(pr_util.commit_ids['new-feature'])
901 964
902 965 response = self.app.post(
903 966 route_path('pullrequest_update',
904 967 repo_name=pull_request.target_repo.repo_name,
905 968 pull_request_id=pull_request.pull_request_id),
906 969 params={'update_commits': 'true',
907 970 'csrf_token': csrf_token})
908 971
909 972 assert response.status_int == 200
910 973 assert response.body == 'true'
911 974
912 975 # Make sure that after update, it won't raise 500 errors
913 976 response = self.app.get(route_path(
914 977 'pullrequest_show',
915 978 repo_name=pr_util.target_repository.repo_name,
916 979 pull_request_id=pull_request.pull_request_id))
917 980
918 981 assert response.status_int == 200
919 982 assert_response = AssertResponse(response)
920 983 assert_response.element_contains(
921 984 '#changeset_compare_view_content .alert strong',
922 985 'Missing commits')
923 986
924 987 def test_branch_is_a_link(self, pr_util):
925 988 pull_request = pr_util.create_pull_request()
926 989 pull_request.source_ref = 'branch:origin:1234567890abcdef'
927 990 pull_request.target_ref = 'branch:target:abcdef1234567890'
928 991 Session().add(pull_request)
929 992 Session().commit()
930 993
931 994 response = self.app.get(route_path(
932 995 'pullrequest_show',
933 996 repo_name=pull_request.target_repo.scm_instance().name,
934 997 pull_request_id=pull_request.pull_request_id))
935 998 assert response.status_int == 200
936 999 assert_response = AssertResponse(response)
937 1000
938 1001 origin = assert_response.get_element('.pr-origininfo .tag')
939 1002 origin_children = origin.getchildren()
940 1003 assert len(origin_children) == 1
941 1004 target = assert_response.get_element('.pr-targetinfo .tag')
942 1005 target_children = target.getchildren()
943 1006 assert len(target_children) == 1
944 1007
945 1008 expected_origin_link = route_path(
946 1009 'repo_changelog',
947 1010 repo_name=pull_request.source_repo.scm_instance().name,
948 1011 params=dict(branch='origin'))
949 1012 expected_target_link = route_path(
950 1013 'repo_changelog',
951 1014 repo_name=pull_request.target_repo.scm_instance().name,
952 1015 params=dict(branch='target'))
953 1016 assert origin_children[0].attrib['href'] == expected_origin_link
954 1017 assert origin_children[0].text == 'branch: origin'
955 1018 assert target_children[0].attrib['href'] == expected_target_link
956 1019 assert target_children[0].text == 'branch: target'
957 1020
958 1021 def test_bookmark_is_not_a_link(self, pr_util):
959 1022 pull_request = pr_util.create_pull_request()
960 1023 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
961 1024 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
962 1025 Session().add(pull_request)
963 1026 Session().commit()
964 1027
965 1028 response = self.app.get(route_path(
966 1029 'pullrequest_show',
967 1030 repo_name=pull_request.target_repo.scm_instance().name,
968 1031 pull_request_id=pull_request.pull_request_id))
969 1032 assert response.status_int == 200
970 1033 assert_response = AssertResponse(response)
971 1034
972 1035 origin = assert_response.get_element('.pr-origininfo .tag')
973 1036 assert origin.text.strip() == 'bookmark: origin'
974 1037 assert origin.getchildren() == []
975 1038
976 1039 target = assert_response.get_element('.pr-targetinfo .tag')
977 1040 assert target.text.strip() == 'bookmark: target'
978 1041 assert target.getchildren() == []
979 1042
980 1043 def test_tag_is_not_a_link(self, pr_util):
981 1044 pull_request = pr_util.create_pull_request()
982 1045 pull_request.source_ref = 'tag:origin:1234567890abcdef'
983 1046 pull_request.target_ref = 'tag:target:abcdef1234567890'
984 1047 Session().add(pull_request)
985 1048 Session().commit()
986 1049
987 1050 response = self.app.get(route_path(
988 1051 'pullrequest_show',
989 1052 repo_name=pull_request.target_repo.scm_instance().name,
990 1053 pull_request_id=pull_request.pull_request_id))
991 1054 assert response.status_int == 200
992 1055 assert_response = AssertResponse(response)
993 1056
994 1057 origin = assert_response.get_element('.pr-origininfo .tag')
995 1058 assert origin.text.strip() == 'tag: origin'
996 1059 assert origin.getchildren() == []
997 1060
998 1061 target = assert_response.get_element('.pr-targetinfo .tag')
999 1062 assert target.text.strip() == 'tag: target'
1000 1063 assert target.getchildren() == []
1001 1064
1002 1065 @pytest.mark.parametrize('mergeable', [True, False])
1003 1066 def test_shadow_repository_link(
1004 1067 self, mergeable, pr_util, http_host_only_stub):
1005 1068 """
1006 1069 Check that the pull request summary page displays a link to the shadow
1007 1070 repository if the pull request is mergeable. If it is not mergeable
1008 1071 the link should not be displayed.
1009 1072 """
1010 1073 pull_request = pr_util.create_pull_request(
1011 1074 mergeable=mergeable, enable_notifications=False)
1012 1075 target_repo = pull_request.target_repo.scm_instance()
1013 1076 pr_id = pull_request.pull_request_id
1014 1077 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1015 1078 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1016 1079
1017 1080 response = self.app.get(route_path(
1018 1081 'pullrequest_show',
1019 1082 repo_name=target_repo.name,
1020 1083 pull_request_id=pr_id))
1021 1084
1022 1085 assertr = AssertResponse(response)
1023 1086 if mergeable:
1024 1087 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1025 1088 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1026 1089 else:
1027 1090 assertr.no_element_exists('.pr-mergeinfo')
1028 1091
1029 1092
1030 1093 @pytest.mark.usefixtures('app')
1031 1094 @pytest.mark.backends("git", "hg")
1032 1095 class TestPullrequestsControllerDelete(object):
1033 1096 def test_pull_request_delete_button_permissions_admin(
1034 1097 self, autologin_user, user_admin, pr_util):
1035 1098 pull_request = pr_util.create_pull_request(
1036 1099 author=user_admin.username, enable_notifications=False)
1037 1100
1038 1101 response = self.app.get(route_path(
1039 1102 'pullrequest_show',
1040 1103 repo_name=pull_request.target_repo.scm_instance().name,
1041 1104 pull_request_id=pull_request.pull_request_id))
1042 1105
1043 1106 response.mustcontain('id="delete_pullrequest"')
1044 1107 response.mustcontain('Confirm to delete this pull request')
1045 1108
1046 1109 def test_pull_request_delete_button_permissions_owner(
1047 1110 self, autologin_regular_user, user_regular, pr_util):
1048 1111 pull_request = pr_util.create_pull_request(
1049 1112 author=user_regular.username, enable_notifications=False)
1050 1113
1051 1114 response = self.app.get(route_path(
1052 1115 'pullrequest_show',
1053 1116 repo_name=pull_request.target_repo.scm_instance().name,
1054 1117 pull_request_id=pull_request.pull_request_id))
1055 1118
1056 1119 response.mustcontain('id="delete_pullrequest"')
1057 1120 response.mustcontain('Confirm to delete this pull request')
1058 1121
1059 1122 def test_pull_request_delete_button_permissions_forbidden(
1060 1123 self, autologin_regular_user, user_regular, user_admin, pr_util):
1061 1124 pull_request = pr_util.create_pull_request(
1062 1125 author=user_admin.username, enable_notifications=False)
1063 1126
1064 1127 response = self.app.get(route_path(
1065 1128 'pullrequest_show',
1066 1129 repo_name=pull_request.target_repo.scm_instance().name,
1067 1130 pull_request_id=pull_request.pull_request_id))
1068 1131 response.mustcontain(no=['id="delete_pullrequest"'])
1069 1132 response.mustcontain(no=['Confirm to delete this pull request'])
1070 1133
1071 1134 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1072 1135 self, autologin_regular_user, user_regular, user_admin, pr_util,
1073 1136 user_util):
1074 1137
1075 1138 pull_request = pr_util.create_pull_request(
1076 1139 author=user_admin.username, enable_notifications=False)
1077 1140
1078 1141 user_util.grant_user_permission_to_repo(
1079 1142 pull_request.target_repo, user_regular,
1080 1143 'repository.write')
1081 1144
1082 1145 response = self.app.get(route_path(
1083 1146 'pullrequest_show',
1084 1147 repo_name=pull_request.target_repo.scm_instance().name,
1085 1148 pull_request_id=pull_request.pull_request_id))
1086 1149
1087 1150 response.mustcontain('id="open_edit_pullrequest"')
1088 1151 response.mustcontain('id="delete_pullrequest"')
1089 1152 response.mustcontain(no=['Confirm to delete this pull request'])
1090 1153
1091 1154 def test_delete_comment_returns_404_if_comment_does_not_exist(
1092 1155 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1093 1156
1094 1157 pull_request = pr_util.create_pull_request(
1095 1158 author=user_admin.username, enable_notifications=False)
1096 1159
1097 1160 self.app.post(
1098 1161 route_path(
1099 1162 'pullrequest_comment_delete',
1100 1163 repo_name=pull_request.target_repo.scm_instance().name,
1101 1164 pull_request_id=pull_request.pull_request_id,
1102 1165 comment_id=1024404),
1103 1166 extra_environ=xhr_header,
1104 1167 params={'csrf_token': csrf_token},
1105 1168 status=404
1106 1169 )
1107 1170
1108 1171 def test_delete_comment(
1109 1172 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1110 1173
1111 1174 pull_request = pr_util.create_pull_request(
1112 1175 author=user_admin.username, enable_notifications=False)
1113 1176 comment = pr_util.create_comment()
1114 1177 comment_id = comment.comment_id
1115 1178
1116 1179 response = self.app.post(
1117 1180 route_path(
1118 1181 'pullrequest_comment_delete',
1119 1182 repo_name=pull_request.target_repo.scm_instance().name,
1120 1183 pull_request_id=pull_request.pull_request_id,
1121 1184 comment_id=comment_id),
1122 1185 extra_environ=xhr_header,
1123 1186 params={'csrf_token': csrf_token},
1124 1187 status=200
1125 1188 )
1126 1189 assert response.body == 'true'
1127 1190
1128 1191
1129 1192 def assert_pull_request_status(pull_request, expected_status):
1130 1193 status = ChangesetStatusModel().calculated_review_status(
1131 1194 pull_request=pull_request)
1132 1195 assert status == expected_status
1133 1196
1134 1197
1135 1198 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1136 1199 @pytest.mark.usefixtures("autologin_user")
1137 1200 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1138 1201 response = app.get(
1139 1202 route_path(route, repo_name=backend_svn.repo_name), status=404)
1140 1203
@@ -1,985 +1,1006 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT repository module
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode.lib.compat import OrderedDict
32 32 from rhodecode.lib.datelib import (
33 33 utcdate_fromtimestamp, makedate, date_astimestamp)
34 34 from rhodecode.lib.utils import safe_unicode, safe_str
35 35 from rhodecode.lib.vcs import connection, path as vcspath
36 36 from rhodecode.lib.vcs.backends.base import (
37 37 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 38 MergeFailureReason, Reference)
39 39 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 40 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 41 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 42 from rhodecode.lib.vcs.exceptions import (
43 43 CommitDoesNotExistError, EmptyRepositoryError,
44 44 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 45
46 46
47 47 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class GitRepository(BaseRepository):
53 53 """
54 54 Git repository backend.
55 55 """
56 56 DEFAULT_BRANCH_NAME = 'master'
57 57
58 58 contact = BaseRepository.DEFAULT_CONTACT
59 59
60 60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 61 update_after_clone=False, with_wire=None, bare=False):
62 62
63 63 self.path = safe_str(os.path.abspath(repo_path))
64 64 self.config = config if config else self.get_default_config()
65 65 self._remote = connection.Git(
66 66 self.path, self.config, with_wire=with_wire)
67 67
68 68 self._init_repo(create, src_url, update_after_clone, bare)
69 69
70 70 # caches
71 71 self._commit_ids = {}
72 72
73 73 @LazyProperty
74 74 def bare(self):
75 75 return self._remote.bare()
76 76
77 77 @LazyProperty
78 78 def head(self):
79 79 return self._remote.head()
80 80
81 81 @LazyProperty
82 82 def commit_ids(self):
83 83 """
84 84 Returns list of commit ids, in ascending order. Being lazy
85 85 attribute allows external tools to inject commit ids from cache.
86 86 """
87 87 commit_ids = self._get_all_commit_ids()
88 88 self._rebuild_cache(commit_ids)
89 89 return commit_ids
90 90
91 91 def _rebuild_cache(self, commit_ids):
92 92 self._commit_ids = dict((commit_id, index)
93 93 for index, commit_id in enumerate(commit_ids))
94 94
95 95 def run_git_command(self, cmd, **opts):
96 96 """
97 97 Runs given ``cmd`` as git command and returns tuple
98 98 (stdout, stderr).
99 99
100 100 :param cmd: git command to be executed
101 101 :param opts: env options to pass into Subprocess command
102 102 """
103 103 if not isinstance(cmd, list):
104 104 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
105 105
106 106 skip_stderr_log = opts.pop('skip_stderr_log', False)
107 107 out, err = self._remote.run_git_command(cmd, **opts)
108 108 if err and not skip_stderr_log:
109 109 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
110 110 return out, err
111 111
112 112 @staticmethod
113 113 def check_url(url, config):
114 114 """
115 115 Function will check given url and try to verify if it's a valid
116 116 link. Sometimes it may happened that git will issue basic
117 117 auth request that can cause whole API to hang when used from python
118 118 or other external calls.
119 119
120 120 On failures it'll raise urllib2.HTTPError, exception is also thrown
121 121 when the return code is non 200
122 122 """
123 123 # check first if it's not an url
124 124 if os.path.isdir(url) or url.startswith('file:'):
125 125 return True
126 126
127 127 if '+' in url.split('://', 1)[0]:
128 128 url = url.split('+', 1)[1]
129 129
130 130 # Request the _remote to verify the url
131 131 return connection.Git.check_url(url, config.serialize())
132 132
133 133 @staticmethod
134 134 def is_valid_repository(path):
135 135 if os.path.isdir(os.path.join(path, '.git')):
136 136 return True
137 137 # check case of bare repository
138 138 try:
139 139 GitRepository(path)
140 140 return True
141 141 except VCSError:
142 142 pass
143 143 return False
144 144
145 145 def _init_repo(self, create, src_url=None, update_after_clone=False,
146 146 bare=False):
147 147 if create and os.path.exists(self.path):
148 148 raise RepositoryError(
149 149 "Cannot create repository at %s, location already exist"
150 150 % self.path)
151 151
152 152 try:
153 153 if create and src_url:
154 154 GitRepository.check_url(src_url, self.config)
155 155 self.clone(src_url, update_after_clone, bare)
156 156 elif create:
157 157 os.makedirs(self.path, mode=0755)
158 158
159 159 if bare:
160 160 self._remote.init_bare()
161 161 else:
162 162 self._remote.init()
163 163 else:
164 164 if not self._remote.assert_correct_path():
165 165 raise RepositoryError(
166 166 'Path "%s" does not contain a Git repository' %
167 167 (self.path,))
168 168
169 169 # TODO: johbo: check if we have to translate the OSError here
170 170 except OSError as err:
171 171 raise RepositoryError(err)
172 172
173 173 def _get_all_commit_ids(self, filters=None):
174 174 # we must check if this repo is not empty, since later command
175 175 # fails if it is. And it's cheaper to ask than throw the subprocess
176 176 # errors
177 177 try:
178 178 self._remote.head()
179 179 except KeyError:
180 180 return []
181 181
182 182 rev_filter = ['--branches', '--tags']
183 183 extra_filter = []
184 184
185 185 if filters:
186 186 if filters.get('since'):
187 187 extra_filter.append('--since=%s' % (filters['since']))
188 188 if filters.get('until'):
189 189 extra_filter.append('--until=%s' % (filters['until']))
190 190 if filters.get('branch_name'):
191 191 rev_filter = ['--tags']
192 192 extra_filter.append(filters['branch_name'])
193 193 rev_filter.extend(extra_filter)
194 194
195 195 # if filters.get('start') or filters.get('end'):
196 196 # # skip is offset, max-count is limit
197 197 # if filters.get('start'):
198 198 # extra_filter += ' --skip=%s' % filters['start']
199 199 # if filters.get('end'):
200 200 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
201 201
202 202 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
203 203 try:
204 204 output, __ = self.run_git_command(cmd)
205 205 except RepositoryError:
206 206 # Can be raised for empty repositories
207 207 return []
208 208 return output.splitlines()
209 209
210 210 def _get_commit_id(self, commit_id_or_idx):
211 211 def is_null(value):
212 212 return len(value) == commit_id_or_idx.count('0')
213 213
214 214 if self.is_empty():
215 215 raise EmptyRepositoryError("There are no commits yet")
216 216
217 217 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
218 218 return self.commit_ids[-1]
219 219
220 220 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
221 221 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
222 222 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
223 223 try:
224 224 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
225 225 except Exception:
226 226 msg = "Commit %s does not exist for %s" % (
227 227 commit_id_or_idx, self)
228 228 raise CommitDoesNotExistError(msg)
229 229
230 230 elif is_bstr:
231 231 # check full path ref, eg. refs/heads/master
232 232 ref_id = self._refs.get(commit_id_or_idx)
233 233 if ref_id:
234 234 return ref_id
235 235
236 236 # check branch name
237 237 branch_ids = self.branches.values()
238 238 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
239 239 if ref_id:
240 240 return ref_id
241 241
242 242 # check tag name
243 243 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
244 244 if ref_id:
245 245 return ref_id
246 246
247 247 if (not SHA_PATTERN.match(commit_id_or_idx) or
248 248 commit_id_or_idx not in self.commit_ids):
249 249 msg = "Commit %s does not exist for %s" % (
250 250 commit_id_or_idx, self)
251 251 raise CommitDoesNotExistError(msg)
252 252
253 253 # Ensure we return full id
254 254 if not SHA_PATTERN.match(str(commit_id_or_idx)):
255 255 raise CommitDoesNotExistError(
256 256 "Given commit id %s not recognized" % commit_id_or_idx)
257 257 return commit_id_or_idx
258 258
259 259 def get_hook_location(self):
260 260 """
261 261 returns absolute path to location where hooks are stored
262 262 """
263 263 loc = os.path.join(self.path, 'hooks')
264 264 if not self.bare:
265 265 loc = os.path.join(self.path, '.git', 'hooks')
266 266 return loc
267 267
268 268 @LazyProperty
269 269 def last_change(self):
270 270 """
271 271 Returns last change made on this repository as
272 272 `datetime.datetime` object.
273 273 """
274 274 try:
275 275 return self.get_commit().date
276 276 except RepositoryError:
277 277 tzoffset = makedate()[1]
278 278 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
279 279
280 280 def _get_fs_mtime(self):
281 281 idx_loc = '' if self.bare else '.git'
282 282 # fallback to filesystem
283 283 in_path = os.path.join(self.path, idx_loc, "index")
284 284 he_path = os.path.join(self.path, idx_loc, "HEAD")
285 285 if os.path.exists(in_path):
286 286 return os.stat(in_path).st_mtime
287 287 else:
288 288 return os.stat(he_path).st_mtime
289 289
290 290 @LazyProperty
291 291 def description(self):
292 292 description = self._remote.get_description()
293 293 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
294 294
295 295 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
296 296 if self.is_empty():
297 297 return OrderedDict()
298 298
299 299 result = []
300 300 for ref, sha in self._refs.iteritems():
301 301 if ref.startswith(prefix):
302 302 ref_name = ref
303 303 if strip_prefix:
304 304 ref_name = ref[len(prefix):]
305 305 result.append((safe_unicode(ref_name), sha))
306 306
307 307 def get_name(entry):
308 308 return entry[0]
309 309
310 310 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
311 311
312 312 def _get_branches(self):
313 313 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
314 314
315 315 @LazyProperty
316 316 def branches(self):
317 317 return self._get_branches()
318 318
319 319 @LazyProperty
320 320 def branches_closed(self):
321 321 return {}
322 322
323 323 @LazyProperty
324 324 def bookmarks(self):
325 325 return {}
326 326
327 327 @LazyProperty
328 328 def branches_all(self):
329 329 all_branches = {}
330 330 all_branches.update(self.branches)
331 331 all_branches.update(self.branches_closed)
332 332 return all_branches
333 333
334 334 @LazyProperty
335 335 def tags(self):
336 336 return self._get_tags()
337 337
338 338 def _get_tags(self):
339 339 return self._get_refs_entries(
340 340 prefix='refs/tags/', strip_prefix=True, reverse=True)
341 341
342 342 def tag(self, name, user, commit_id=None, message=None, date=None,
343 343 **kwargs):
344 344 # TODO: fix this method to apply annotated tags correct with message
345 345 """
346 346 Creates and returns a tag for the given ``commit_id``.
347 347
348 348 :param name: name for new tag
349 349 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
350 350 :param commit_id: commit id for which new tag would be created
351 351 :param message: message of the tag's commit
352 352 :param date: date of tag's commit
353 353
354 354 :raises TagAlreadyExistError: if tag with same name already exists
355 355 """
356 356 if name in self.tags:
357 357 raise TagAlreadyExistError("Tag %s already exists" % name)
358 358 commit = self.get_commit(commit_id=commit_id)
359 359 message = message or "Added tag %s for commit %s" % (
360 360 name, commit.raw_id)
361 361 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
362 362
363 363 self._refs = self._get_refs()
364 364 self.tags = self._get_tags()
365 365 return commit
366 366
367 367 def remove_tag(self, name, user, message=None, date=None):
368 368 """
369 369 Removes tag with the given ``name``.
370 370
371 371 :param name: name of the tag to be removed
372 372 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
373 373 :param message: message of the tag's removal commit
374 374 :param date: date of tag's removal commit
375 375
376 376 :raises TagDoesNotExistError: if tag with given name does not exists
377 377 """
378 378 if name not in self.tags:
379 379 raise TagDoesNotExistError("Tag %s does not exist" % name)
380 380 tagpath = vcspath.join(
381 381 self._remote.get_refs_path(), 'refs', 'tags', name)
382 382 try:
383 383 os.remove(tagpath)
384 384 self._refs = self._get_refs()
385 385 self.tags = self._get_tags()
386 386 except OSError as e:
387 387 raise RepositoryError(e.strerror)
388 388
389 389 def _get_refs(self):
390 390 return self._remote.get_refs()
391 391
392 392 @LazyProperty
393 393 def _refs(self):
394 394 return self._get_refs()
395 395
396 396 @property
397 397 def _ref_tree(self):
398 398 node = tree = {}
399 399 for ref, sha in self._refs.iteritems():
400 400 path = ref.split('/')
401 401 for bit in path[:-1]:
402 402 node = node.setdefault(bit, {})
403 403 node[path[-1]] = sha
404 404 node = tree
405 405 return tree
406 406
407 407 def get_remote_ref(self, ref_name):
408 408 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
409 409 try:
410 410 return self._refs[ref_key]
411 411 except Exception:
412 412 return
413 413
414 414 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
415 415 """
416 416 Returns `GitCommit` object representing commit from git repository
417 417 at the given `commit_id` or head (most recent commit) if None given.
418 418 """
419 419 if commit_id is not None:
420 420 self._validate_commit_id(commit_id)
421 421 elif commit_idx is not None:
422 422 self._validate_commit_idx(commit_idx)
423 423 commit_id = commit_idx
424 424 commit_id = self._get_commit_id(commit_id)
425 425 try:
426 426 # Need to call remote to translate id for tagging scenario
427 427 commit_id = self._remote.get_object(commit_id)["commit_id"]
428 428 idx = self._commit_ids[commit_id]
429 429 except KeyError:
430 430 raise RepositoryError("Cannot get object with id %s" % commit_id)
431 431
432 432 return GitCommit(self, commit_id, idx, pre_load=pre_load)
433 433
434 434 def get_commits(
435 435 self, start_id=None, end_id=None, start_date=None, end_date=None,
436 436 branch_name=None, show_hidden=False, pre_load=None):
437 437 """
438 438 Returns generator of `GitCommit` objects from start to end (both
439 439 are inclusive), in ascending date order.
440 440
441 441 :param start_id: None, str(commit_id)
442 442 :param end_id: None, str(commit_id)
443 443 :param start_date: if specified, commits with commit date less than
444 444 ``start_date`` would be filtered out from returned set
445 445 :param end_date: if specified, commits with commit date greater than
446 446 ``end_date`` would be filtered out from returned set
447 447 :param branch_name: if specified, commits not reachable from given
448 448 branch would be filtered out from returned set
449 449 :param show_hidden: Show hidden commits such as obsolete or hidden from
450 450 Mercurial evolve
451 451 :raise BranchDoesNotExistError: If given `branch_name` does not
452 452 exist.
453 453 :raise CommitDoesNotExistError: If commits for given `start` or
454 454 `end` could not be found.
455 455
456 456 """
457 457 if self.is_empty():
458 458 raise EmptyRepositoryError("There are no commits yet")
459 459 self._validate_branch_name(branch_name)
460 460
461 461 if start_id is not None:
462 462 self._validate_commit_id(start_id)
463 463 if end_id is not None:
464 464 self._validate_commit_id(end_id)
465 465
466 466 start_raw_id = self._get_commit_id(start_id)
467 467 start_pos = self._commit_ids[start_raw_id] if start_id else None
468 468 end_raw_id = self._get_commit_id(end_id)
469 469 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
470 470
471 471 if None not in [start_id, end_id] and start_pos > end_pos:
472 472 raise RepositoryError(
473 473 "Start commit '%s' cannot be after end commit '%s'" %
474 474 (start_id, end_id))
475 475
476 476 if end_pos is not None:
477 477 end_pos += 1
478 478
479 479 filter_ = []
480 480 if branch_name:
481 481 filter_.append({'branch_name': branch_name})
482 482 if start_date and not end_date:
483 483 filter_.append({'since': start_date})
484 484 if end_date and not start_date:
485 485 filter_.append({'until': end_date})
486 486 if start_date and end_date:
487 487 filter_.append({'since': start_date})
488 488 filter_.append({'until': end_date})
489 489
490 490 # if start_pos or end_pos:
491 491 # filter_.append({'start': start_pos})
492 492 # filter_.append({'end': end_pos})
493 493
494 494 if filter_:
495 495 revfilters = {
496 496 'branch_name': branch_name,
497 497 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
498 498 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
499 499 'start': start_pos,
500 500 'end': end_pos,
501 501 }
502 502 commit_ids = self._get_all_commit_ids(filters=revfilters)
503 503
504 504 # pure python stuff, it's slow due to walker walking whole repo
505 505 # def get_revs(walker):
506 506 # for walker_entry in walker:
507 507 # yield walker_entry.commit.id
508 508 # revfilters = {}
509 509 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
510 510 else:
511 511 commit_ids = self.commit_ids
512 512
513 513 if start_pos or end_pos:
514 514 commit_ids = commit_ids[start_pos: end_pos]
515 515
516 516 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
517 517
518 518 def get_diff(
519 519 self, commit1, commit2, path='', ignore_whitespace=False,
520 520 context=3, path1=None):
521 521 """
522 522 Returns (git like) *diff*, as plain text. Shows changes introduced by
523 523 ``commit2`` since ``commit1``.
524 524
525 525 :param commit1: Entry point from which diff is shown. Can be
526 526 ``self.EMPTY_COMMIT`` - in this case, patch showing all
527 527 the changes since empty state of the repository until ``commit2``
528 528 :param commit2: Until which commits changes should be shown.
529 529 :param ignore_whitespace: If set to ``True``, would not show whitespace
530 530 changes. Defaults to ``False``.
531 531 :param context: How many lines before/after changed lines should be
532 532 shown. Defaults to ``3``.
533 533 """
534 534 self._validate_diff_commits(commit1, commit2)
535 535 if path1 is not None and path1 != path:
536 536 raise ValueError("Diff of two different paths not supported.")
537 537
538 538 flags = [
539 539 '-U%s' % context, '--full-index', '--binary', '-p',
540 540 '-M', '--abbrev=40']
541 541 if ignore_whitespace:
542 542 flags.append('-w')
543 543
544 544 if commit1 == self.EMPTY_COMMIT:
545 545 cmd = ['show'] + flags + [commit2.raw_id]
546 546 else:
547 547 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
548 548
549 549 if path:
550 550 cmd.extend(['--', path])
551 551
552 552 stdout, __ = self.run_git_command(cmd)
553 553 # If we used 'show' command, strip first few lines (until actual diff
554 554 # starts)
555 555 if commit1 == self.EMPTY_COMMIT:
556 556 lines = stdout.splitlines()
557 557 x = 0
558 558 for line in lines:
559 559 if line.startswith('diff'):
560 560 break
561 561 x += 1
562 562 # Append new line just like 'diff' command do
563 563 stdout = '\n'.join(lines[x:]) + '\n'
564 564 return GitDiff(stdout)
565 565
566 566 def strip(self, commit_id, branch_name):
567 567 commit = self.get_commit(commit_id=commit_id)
568 568 if commit.merge:
569 569 raise Exception('Cannot reset to merge commit')
570 570
571 571 # parent is going to be the new head now
572 572 commit = commit.parents[0]
573 573 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
574 574
575 575 self.commit_ids = self._get_all_commit_ids()
576 576 self._rebuild_cache(self.commit_ids)
577 577
578 578 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
579 579 if commit_id1 == commit_id2:
580 580 return commit_id1
581 581
582 582 if self != repo2:
583 583 commits = self._remote.get_missing_revs(
584 584 commit_id1, commit_id2, repo2.path)
585 585 if commits:
586 586 commit = repo2.get_commit(commits[-1])
587 587 if commit.parents:
588 588 ancestor_id = commit.parents[0].raw_id
589 589 else:
590 590 ancestor_id = None
591 591 else:
592 592 # no commits from other repo, ancestor_id is the commit_id2
593 593 ancestor_id = commit_id2
594 594 else:
595 595 output, __ = self.run_git_command(
596 596 ['merge-base', commit_id1, commit_id2])
597 597 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
598 598
599 599 return ancestor_id
600 600
601 601 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
602 602 repo1 = self
603 603 ancestor_id = None
604 604
605 605 if commit_id1 == commit_id2:
606 606 commits = []
607 607 elif repo1 != repo2:
608 608 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
609 609 repo2.path)
610 610 commits = [
611 611 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
612 612 for commit_id in reversed(missing_ids)]
613 613 else:
614 614 output, __ = repo1.run_git_command(
615 615 ['log', '--reverse', '--pretty=format: %H', '-s',
616 616 '%s..%s' % (commit_id1, commit_id2)])
617 617 commits = [
618 618 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
619 619 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
620 620
621 621 return commits
622 622
623 623 @LazyProperty
624 624 def in_memory_commit(self):
625 625 """
626 626 Returns ``GitInMemoryCommit`` object for this repository.
627 627 """
628 628 return GitInMemoryCommit(self)
629 629
630 630 def clone(self, url, update_after_clone=True, bare=False):
631 631 """
632 632 Tries to clone commits from external location.
633 633
634 634 :param update_after_clone: If set to ``False``, git won't checkout
635 635 working directory
636 636 :param bare: If set to ``True``, repository would be cloned into
637 637 *bare* git repository (no working directory at all).
638 638 """
639 639 # init_bare and init expect empty dir created to proceed
640 640 if not os.path.exists(self.path):
641 641 os.mkdir(self.path)
642 642
643 643 if bare:
644 644 self._remote.init_bare()
645 645 else:
646 646 self._remote.init()
647 647
648 648 deferred = '^{}'
649 649 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
650 650
651 651 return self._remote.clone(
652 652 url, deferred, valid_refs, update_after_clone)
653 653
654 654 def pull(self, url, commit_ids=None):
655 655 """
656 656 Tries to pull changes from external location. We use fetch here since
657 657 pull in get does merges and we want to be compatible with hg backend so
658 658 pull == fetch in this case
659 659 """
660 660 self.fetch(url, commit_ids=commit_ids)
661 661
662 662 def fetch(self, url, commit_ids=None):
663 663 """
664 664 Tries to fetch changes from external location.
665 665 """
666 666 refs = None
667 667
668 668 if commit_ids is not None:
669 669 remote_refs = self._remote.get_remote_refs(url)
670 670 refs = [
671 671 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
672 672 self._remote.fetch(url, refs=refs)
673 673
674 674 def push(self, url):
675 675 refs = None
676 676 self._remote.sync_push(url, refs=refs)
677 677
678 678 def set_refs(self, ref_name, commit_id):
679 679 self._remote.set_refs(ref_name, commit_id)
680 680
681 681 def remove_ref(self, ref_name):
682 682 self._remote.remove_ref(ref_name)
683 683
684 684 def _update_server_info(self):
685 685 """
686 686 runs gits update-server-info command in this repo instance
687 687 """
688 688 self._remote.update_server_info()
689 689
690 690 def _current_branch(self):
691 691 """
692 692 Return the name of the current branch.
693 693
694 694 It only works for non bare repositories (i.e. repositories with a
695 695 working copy)
696 696 """
697 697 if self.bare:
698 698 raise RepositoryError('Bare git repos do not have active branches')
699 699
700 700 if self.is_empty():
701 701 return None
702 702
703 703 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
704 704 return stdout.strip()
705 705
706 706 def _checkout(self, branch_name, create=False, force=False):
707 707 """
708 708 Checkout a branch in the working directory.
709 709
710 710 It tries to create the branch if create is True, failing if the branch
711 711 already exists.
712 712
713 713 It only works for non bare repositories (i.e. repositories with a
714 714 working copy)
715 715 """
716 716 if self.bare:
717 717 raise RepositoryError('Cannot checkout branches in a bare git repo')
718 718
719 719 cmd = ['checkout']
720 720 if force:
721 721 cmd.append('-f')
722 722 if create:
723 723 cmd.append('-b')
724 724 cmd.append(branch_name)
725 725 self.run_git_command(cmd, fail_on_stderr=False)
726 726
727 727 def _identify(self):
728 728 """
729 729 Return the current state of the working directory.
730 730 """
731 731 if self.bare:
732 732 raise RepositoryError('Bare git repos do not have active branches')
733 733
734 734 if self.is_empty():
735 735 return None
736 736
737 737 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
738 738 return stdout.strip()
739 739
740 740 def _local_clone(self, clone_path, branch_name, source_branch=None):
741 741 """
742 742 Create a local clone of the current repo.
743 743 """
744 744 # N.B.(skreft): the --branch option is required as otherwise the shallow
745 745 # clone will only fetch the active branch.
746 746 cmd = ['clone', '--branch', branch_name,
747 747 self.path, os.path.abspath(clone_path)]
748 748
749 749 self.run_git_command(cmd, fail_on_stderr=False)
750 750
751 751 # if we get the different source branch, make sure we also fetch it for
752 752 # merge conditions
753 753 if source_branch and source_branch != branch_name:
754 754 # check if the ref exists.
755 755 shadow_repo = GitRepository(os.path.abspath(clone_path))
756 756 if shadow_repo.get_remote_ref(source_branch):
757 757 cmd = ['fetch', self.path, source_branch]
758 758 self.run_git_command(cmd, fail_on_stderr=False)
759 759
760 def _local_fetch(self, repository_path, branch_name):
760 def _local_fetch(self, repository_path, branch_name, use_origin=False):
761 761 """
762 762 Fetch a branch from a local repository.
763 763 """
764 764 repository_path = os.path.abspath(repository_path)
765 765 if repository_path == self.path:
766 766 raise ValueError('Cannot fetch from the same repository')
767 767
768 cmd = ['fetch', '--no-tags', repository_path, branch_name]
768 if use_origin:
769 branch_name = '+{branch}:refs/heads/{branch}'.format(
770 branch=branch_name)
771
772 cmd = ['fetch', '--no-tags', '--update-head-ok',
773 repository_path, branch_name]
774 self.run_git_command(cmd, fail_on_stderr=False)
775
776 def _local_reset(self, branch_name):
777 branch_name = '{}'.format(branch_name)
778 cmd = ['reset', '--hard', branch_name]
769 779 self.run_git_command(cmd, fail_on_stderr=False)
770 780
771 781 def _last_fetch_heads(self):
772 782 """
773 783 Return the last fetched heads that need merging.
774 784
775 785 The algorithm is defined at
776 786 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
777 787 """
778 788 if not self.bare:
779 789 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
780 790 else:
781 791 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
782 792
783 793 heads = []
784 794 with open(fetch_heads_path) as f:
785 795 for line in f:
786 796 if ' not-for-merge ' in line:
787 797 continue
788 798 line = re.sub('\t.*', '', line, flags=re.DOTALL)
789 799 heads.append(line)
790 800
791 801 return heads
792 802
793 803 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
794 804 return GitRepository(shadow_repository_path)
795 805
796 806 def _local_pull(self, repository_path, branch_name, ff_only=True):
797 807 """
798 808 Pull a branch from a local repository.
799 809 """
800 810 if self.bare:
801 811 raise RepositoryError('Cannot pull into a bare git repository')
802 812 # N.B.(skreft): The --ff-only option is to make sure this is a
803 813 # fast-forward (i.e., we are only pulling new changes and there are no
804 814 # conflicts with our current branch)
805 815 # Additionally, that option needs to go before --no-tags, otherwise git
806 816 # pull complains about it being an unknown flag.
807 817 cmd = ['pull']
808 818 if ff_only:
809 819 cmd.append('--ff-only')
810 820 cmd.extend(['--no-tags', repository_path, branch_name])
811 821 self.run_git_command(cmd, fail_on_stderr=False)
812 822
813 823 def _local_merge(self, merge_message, user_name, user_email, heads):
814 824 """
815 825 Merge the given head into the checked out branch.
816 826
817 827 It will force a merge commit.
818 828
819 829 Currently it raises an error if the repo is empty, as it is not possible
820 830 to create a merge commit in an empty repo.
821 831
822 832 :param merge_message: The message to use for the merge commit.
823 833 :param heads: the heads to merge.
824 834 """
825 835 if self.bare:
826 836 raise RepositoryError('Cannot merge into a bare git repository')
827 837
828 838 if not heads:
829 839 return
830 840
831 841 if self.is_empty():
832 842 # TODO(skreft): do somehting more robust in this case.
833 843 raise RepositoryError(
834 844 'Do not know how to merge into empty repositories yet')
835 845
836 846 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
837 847 # commit message. We also specify the user who is doing the merge.
838 848 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
839 849 '-c', 'user.email=%s' % safe_str(user_email),
840 850 'merge', '--no-ff', '-m', safe_str(merge_message)]
841 851 cmd.extend(heads)
842 852 try:
843 self.run_git_command(cmd, fail_on_stderr=False)
853 output = self.run_git_command(cmd, fail_on_stderr=False)
844 854 except RepositoryError:
845 855 # Cleanup any merge leftovers
846 856 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
847 857 raise
848 858
849 859 def _local_push(
850 860 self, source_branch, repository_path, target_branch,
851 861 enable_hooks=False, rc_scm_data=None):
852 862 """
853 863 Push the source_branch to the given repository and target_branch.
854 864
855 865 Currently it if the target_branch is not master and the target repo is
856 866 empty, the push will work, but then GitRepository won't be able to find
857 867 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
858 868 pointing to master, which does not exist).
859 869
860 870 It does not run the hooks in the target repo.
861 871 """
862 872 # TODO(skreft): deal with the case in which the target repo is empty,
863 873 # and the target_branch is not master.
864 874 target_repo = GitRepository(repository_path)
865 875 if (not target_repo.bare and
866 876 target_repo._current_branch() == target_branch):
867 877 # Git prevents pushing to the checked out branch, so simulate it by
868 878 # pulling into the target repository.
869 879 target_repo._local_pull(self.path, source_branch)
870 880 else:
871 881 cmd = ['push', os.path.abspath(repository_path),
872 882 '%s:%s' % (source_branch, target_branch)]
873 883 gitenv = {}
874 884 if rc_scm_data:
875 885 gitenv.update({'RC_SCM_DATA': rc_scm_data})
876 886
877 887 if not enable_hooks:
878 888 gitenv['RC_SKIP_HOOKS'] = '1'
879 889 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
880 890
881 891 def _get_new_pr_branch(self, source_branch, target_branch):
882 892 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
883 893 pr_branches = []
884 894 for branch in self.branches:
885 895 if branch.startswith(prefix):
886 896 pr_branches.append(int(branch[len(prefix):]))
887 897
888 898 if not pr_branches:
889 899 branch_id = 0
890 900 else:
891 901 branch_id = max(pr_branches) + 1
892 902
893 903 return '%s%d' % (prefix, branch_id)
894 904
895 905 def _merge_repo(self, shadow_repository_path, target_ref,
896 906 source_repo, source_ref, merge_message,
897 907 merger_name, merger_email, dry_run=False,
898 908 use_rebase=False, close_branch=False):
899 909 if target_ref.commit_id != self.branches[target_ref.name]:
910 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
911 target_ref.commit_id, self.branches[target_ref.name])
900 912 return MergeResponse(
901 913 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
902 914
903 915 shadow_repo = GitRepository(shadow_repository_path)
904 916 # checkout source, if it's different. Otherwise we could not
905 917 # fetch proper commits for merge testing
906 918 if source_ref.name != target_ref.name:
907 919 if shadow_repo.get_remote_ref(source_ref.name):
908 920 shadow_repo._checkout(source_ref.name, force=True)
909 921
910 # checkout target
922 # checkout target, and fetch changes
911 923 shadow_repo._checkout(target_ref.name, force=True)
912 shadow_repo._local_pull(self.path, target_ref.name)
924
925 # fetch/reset pull the target, in case it is changed
926 # this handles even force changes
927 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
928 shadow_repo._local_reset(target_ref.name)
913 929
914 930 # Need to reload repo to invalidate the cache, or otherwise we cannot
915 931 # retrieve the last target commit.
916 932 shadow_repo = GitRepository(shadow_repository_path)
917 933 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
934 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
935 target_ref, target_ref.commit_id,
936 shadow_repo.branches[target_ref.name])
918 937 return MergeResponse(
919 938 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
920 939
940 # calculate new branch
921 941 pr_branch = shadow_repo._get_new_pr_branch(
922 942 source_ref.name, target_ref.name)
923 943 log.debug('using pull-request merge branch: `%s`', pr_branch)
944 # checkout to temp branch, and fetch changes
924 945 shadow_repo._checkout(pr_branch, create=True)
925 946 try:
926 947 shadow_repo._local_fetch(source_repo.path, source_ref.name)
927 948 except RepositoryError:
928 949 log.exception('Failure when doing local fetch on git shadow repo')
929 950 return MergeResponse(
930 951 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
931 952
932 953 merge_ref = None
933 954 merge_failure_reason = MergeFailureReason.NONE
934 955 try:
935 956 shadow_repo._local_merge(merge_message, merger_name, merger_email,
936 957 [source_ref.commit_id])
937 958 merge_possible = True
938 959
939 960 # Need to reload repo to invalidate the cache, or otherwise we
940 961 # cannot retrieve the merge commit.
941 962 shadow_repo = GitRepository(shadow_repository_path)
942 963 merge_commit_id = shadow_repo.branches[pr_branch]
943 964
944 965 # Set a reference pointing to the merge commit. This reference may
945 966 # be used to easily identify the last successful merge commit in
946 967 # the shadow repository.
947 968 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
948 969 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
949 970 except RepositoryError:
950 971 log.exception('Failure when doing local merge on git shadow repo')
951 972 merge_possible = False
952 973 merge_failure_reason = MergeFailureReason.MERGE_FAILED
953 974
954 975 if merge_possible and not dry_run:
955 976 try:
956 977 shadow_repo._local_push(
957 978 pr_branch, self.path, target_ref.name, enable_hooks=True,
958 979 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
959 980 merge_succeeded = True
960 981 except RepositoryError:
961 982 log.exception(
962 983 'Failure when doing local push on git shadow repo')
963 984 merge_succeeded = False
964 985 merge_failure_reason = MergeFailureReason.PUSH_FAILED
965 986 else:
966 987 merge_succeeded = False
967 988
968 989 return MergeResponse(
969 990 merge_possible, merge_succeeded, merge_ref,
970 991 merge_failure_reason)
971 992
972 993 def _get_shadow_repository_path(self, workspace_id):
973 994 # The name of the shadow repository must start with '.', so it is
974 995 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
975 996 return os.path.join(
976 997 os.path.dirname(self.path),
977 998 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
978 999
979 1000 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
980 1001 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
981 1002 if not os.path.exists(shadow_repository_path):
982 1003 self._local_clone(
983 1004 shadow_repository_path, target_ref.name, source_ref.name)
984 1005
985 1006 return shadow_repository_path
@@ -1,855 +1,856 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Source')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 %if h.is_hg(c.pull_request.source_repo):
93 93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
94 94 %elif h.is_git(c.pull_request.source_repo):
95 95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
96 96 %endif
97 97
98 98 <div class="">
99 99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
100 100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
101 101 </div>
102 102
103 103 </div>
104 104 </div>
105 105 <div class="field">
106 106 <div class="label-summary">
107 107 <label>${_('Target')}:</label>
108 108 </div>
109 109 <div class="input">
110 110 <div class="pr-targetinfo">
111 111 ## branch link is only valid if it is a branch
112 112 <span class="tag">
113 113 %if c.pull_request.target_ref_parts.type == 'branch':
114 114 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
115 115 %else:
116 116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 117 %endif
118 118 </span>
119 119 <span class="clone-url">
120 120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
121 121 </span>
122 122 </div>
123 123 </div>
124 124 </div>
125 125
126 126 ## Link to the shadow repository.
127 127 <div class="field">
128 128 <div class="label-summary">
129 129 <label>${_('Merge')}:</label>
130 130 </div>
131 131 <div class="input">
132 132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 133 %if h.is_hg(c.pull_request.target_repo):
134 134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
135 135 %elif h.is_git(c.pull_request.target_repo):
136 136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
137 137 %endif
138 138 <div class="">
139 139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
140 140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
141 141 </div>
142 142 % else:
143 143 <div class="">
144 144 ${_('Shadow repository data not available')}.
145 145 </div>
146 146 % endif
147 147 </div>
148 148 </div>
149 149
150 150 <div class="field">
151 151 <div class="label-summary">
152 152 <label>${_('Review')}:</label>
153 153 </div>
154 154 <div class="input">
155 155 %if c.pull_request_review_status:
156 156 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
157 157 <span class="changeset-status-lbl tooltip">
158 158 %if c.pull_request.is_closed():
159 159 ${_('Closed')},
160 160 %endif
161 161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 162 </span>
163 163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
164 164 %endif
165 165 </div>
166 166 </div>
167 167 <div class="field">
168 168 <div class="pr-description-label label-summary">
169 169 <label>${_('Description')}:</label>
170 170 </div>
171 171 <div id="pr-desc" class="input">
172 172 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
173 173 </div>
174 174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
175 175 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
176 176 </div>
177 177 </div>
178 178
179 179 <div class="field">
180 180 <div class="label-summary">
181 181 <label>${_('Versions')}:</label>
182 182 </div>
183 183
184 184 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
185 185 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
186 186
187 187 <div class="pr-versions">
188 188 % if c.show_version_changes:
189 189 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
190 190 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
191 191 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
192 192 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
193 193 data-toggle-off="${_('Hide all versions of this pull request')}">
194 194 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
195 195 </a>
196 196 <table>
197 197 ## SHOW ALL VERSIONS OF PR
198 198 <% ver_pr = None %>
199 199
200 200 % for data in reversed(list(enumerate(c.versions, 1))):
201 201 <% ver_pos = data[0] %>
202 202 <% ver = data[1] %>
203 203 <% ver_pr = ver.pull_request_version_id %>
204 204 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
205 205
206 206 <tr class="version-pr" style="display: ${display_row}">
207 207 <td>
208 208 <code>
209 209 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
210 210 </code>
211 211 </td>
212 212 <td>
213 213 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
214 214 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
215 215 </td>
216 216 <td>
217 217 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
218 218 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
219 219 </div>
220 220 </td>
221 221 <td>
222 222 % if c.at_version_num != ver_pr:
223 223 <i class="icon-comment"></i>
224 224 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
225 225 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
226 226 </code>
227 227 % endif
228 228 </td>
229 229 <td>
230 230 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
231 231 </td>
232 232 <td>
233 233 ${h.age_component(ver.updated_on, time_is_local=True)}
234 234 </td>
235 235 </tr>
236 236 % endfor
237 237
238 238 <tr>
239 239 <td colspan="6">
240 240 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
241 241 data-label-text-locked="${_('select versions to show changes')}"
242 242 data-label-text-diff="${_('show changes between versions')}"
243 243 data-label-text-show="${_('show pull request for this version')}"
244 244 >
245 245 ${_('select versions to show changes')}
246 246 </button>
247 247 </td>
248 248 </tr>
249 249
250 250 ## show comment/inline comments summary
251 251 <%def name="comments_summary()">
252 252 <tr>
253 253 <td colspan="6" class="comments-summary-td">
254 254
255 255 % if c.at_version:
256 256 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
257 257 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
258 258 ${_('Comments at this version')}:
259 259 % else:
260 260 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
261 261 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
262 262 ${_('Comments for this pull request')}:
263 263 % endif
264 264
265 265
266 266 %if general_comm_count_ver:
267 267 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
268 268 %else:
269 269 ${_("%d General ") % general_comm_count_ver}
270 270 %endif
271 271
272 272 %if inline_comm_count_ver:
273 273 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
274 274 %else:
275 275 , ${_("%d Inline") % inline_comm_count_ver}
276 276 %endif
277 277
278 278 %if outdated_comm_count_ver:
279 279 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
280 280 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
281 281 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
282 282 %else:
283 283 , ${_("%d Outdated") % outdated_comm_count_ver}
284 284 %endif
285 285 </td>
286 286 </tr>
287 287 </%def>
288 288 ${comments_summary()}
289 289 </table>
290 290 % else:
291 291 <div class="input">
292 292 ${_('Pull request versions not available')}.
293 293 </div>
294 294 <div>
295 295 <table>
296 296 ${comments_summary()}
297 297 </table>
298 298 </div>
299 299 % endif
300 300 </div>
301 301 </div>
302 302
303 303 <div id="pr-save" class="field" style="display: none;">
304 304 <div class="label-summary"></div>
305 305 <div class="input">
306 306 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
307 307 </div>
308 308 </div>
309 309 </div>
310 310 </div>
311 311 <div>
312 312 ## AUTHOR
313 313 <div class="reviewers-title block-right">
314 314 <div class="pr-details-title">
315 315 ${_('Author of this pull request')}
316 316 </div>
317 317 </div>
318 318 <div class="block-right pr-details-content reviewers">
319 319 <ul class="group_members">
320 320 <li>
321 321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
322 322 </li>
323 323 </ul>
324 324 </div>
325 325
326 326 ## REVIEW RULES
327 327 <div id="review_rules" style="display: none" class="reviewers-title block-right">
328 328 <div class="pr-details-title">
329 329 ${_('Reviewer rules')}
330 330 %if c.allowed_to_update:
331 331 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
332 332 %endif
333 333 </div>
334 334 <div class="pr-reviewer-rules">
335 335 ## review rules will be appended here, by default reviewers logic
336 336 </div>
337 337 <input id="review_data" type="hidden" name="review_data" value="">
338 338 </div>
339 339
340 340 ## REVIEWERS
341 341 <div class="reviewers-title block-right">
342 342 <div class="pr-details-title">
343 343 ${_('Pull request reviewers')}
344 344 %if c.allowed_to_update:
345 345 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
346 346 %endif
347 347 </div>
348 348 </div>
349 349 <div id="reviewers" class="block-right pr-details-content reviewers">
350 350
351 351 ## members redering block
352 352 <input type="hidden" name="__start__" value="review_members:sequence">
353 353 <ul id="review_members" class="group_members">
354 354
355 355 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
356 356 <script>
357 357 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
358 358 var status = "${(status[0][1].status if status else 'not_reviewed')}";
359 359 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
360 360 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
361 361
362 362 var entry = renderTemplate('reviewMemberEntry', {
363 363 'member': member,
364 364 'mandatory': member.mandatory,
365 365 'reasons': member.reasons,
366 366 'allowed_to_update': allowed_to_update,
367 367 'review_status': status,
368 368 'review_status_label': status_lbl,
369 369 'user_group': member.user_group,
370 370 'create': false
371 371 });
372 372 $('#review_members').append(entry)
373 373 </script>
374 374
375 375 % endfor
376 376
377 377 </ul>
378 378 <input type="hidden" name="__end__" value="review_members:sequence">
379 379 ## end members redering block
380 380
381 381 %if not c.pull_request.is_closed():
382 382 <div id="add_reviewer" class="ac" style="display: none;">
383 383 %if c.allowed_to_update:
384 384 % if not c.forbid_adding_reviewers:
385 385 <div id="add_reviewer_input" class="reviewer_ac">
386 386 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
387 387 <div id="reviewers_container"></div>
388 388 </div>
389 389 % endif
390 390 <div class="pull-right">
391 391 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
392 392 </div>
393 393 %endif
394 394 </div>
395 395 %endif
396 396 </div>
397 397 </div>
398 398 </div>
399 399 <div class="box">
400 400 ##DIFF
401 401 <div class="table" >
402 402 <div id="changeset_compare_view_content">
403 403 ##CS
404 404 % if c.missing_requirements:
405 405 <div class="box">
406 406 <div class="alert alert-warning">
407 407 <div>
408 408 <strong>${_('Missing requirements:')}</strong>
409 409 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
410 410 </div>
411 411 </div>
412 412 </div>
413 413 % elif c.missing_commits:
414 414 <div class="box">
415 415 <div class="alert alert-warning">
416 416 <div>
417 417 <strong>${_('Missing commits')}:</strong>
418 418 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
419 419 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
420 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
420 421 </div>
421 422 </div>
422 423 </div>
423 424 % endif
424 425
425 426 <div class="compare_view_commits_title">
426 427 % if not c.compare_mode:
427 428
428 429 % if c.at_version_pos:
429 430 <h4>
430 431 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
431 432 </h4>
432 433 % endif
433 434
434 435 <div class="pull-left">
435 436 <div class="btn-group">
436 437 <a
437 438 class="btn"
438 439 href="#"
439 440 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
440 441 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
441 442 </a>
442 443 <a
443 444 class="btn"
444 445 href="#"
445 446 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
446 447 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 448 </a>
448 449 </div>
449 450 </div>
450 451
451 452 <div class="pull-right">
452 453 % if c.allowed_to_update and not c.pull_request.is_closed():
453 454 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
454 455 % else:
455 456 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
456 457 % endif
457 458
458 459 </div>
459 460 % endif
460 461 </div>
461 462
462 463 % if not c.missing_commits:
463 464 % if c.compare_mode:
464 465 % if c.at_version:
465 466 <h4>
466 467 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
467 468 </h4>
468 469
469 470 <div class="subtitle-compare">
470 471 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
471 472 </div>
472 473
473 474 <div class="container">
474 475 <table class="rctable compare_view_commits">
475 476 <tr>
476 477 <th></th>
477 478 <th>${_('Time')}</th>
478 479 <th>${_('Author')}</th>
479 480 <th>${_('Commit')}</th>
480 481 <th></th>
481 482 <th>${_('Description')}</th>
482 483 </tr>
483 484
484 485 % for c_type, commit in c.commit_changes:
485 486 % if c_type in ['a', 'r']:
486 487 <%
487 488 if c_type == 'a':
488 489 cc_title = _('Commit added in displayed changes')
489 490 elif c_type == 'r':
490 491 cc_title = _('Commit removed in displayed changes')
491 492 else:
492 493 cc_title = ''
493 494 %>
494 495 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
495 496 <td>
496 497 <div class="commit-change-indicator color-${c_type}-border">
497 498 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
498 499 ${c_type.upper()}
499 500 </div>
500 501 </div>
501 502 </td>
502 503 <td class="td-time">
503 504 ${h.age_component(commit.date)}
504 505 </td>
505 506 <td class="td-user">
506 507 ${base.gravatar_with_user(commit.author, 16)}
507 508 </td>
508 509 <td class="td-hash">
509 510 <code>
510 511 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
511 512 r${commit.revision}:${h.short_id(commit.raw_id)}
512 513 </a>
513 514 ${h.hidden('revisions', commit.raw_id)}
514 515 </code>
515 516 </td>
516 517 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
517 518 <div class="show_more_col">
518 519 <i class="show_more"></i>
519 520 </div>
520 521 </td>
521 522 <td class="mid td-description">
522 523 <div class="log-container truncate-wrap">
523 524 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
524 525 ${h.urlify_commit_message(commit.message, c.repo_name)}
525 526 </div>
526 527 </div>
527 528 </td>
528 529 </tr>
529 530 % endif
530 531 % endfor
531 532 </table>
532 533 </div>
533 534
534 535 <script>
535 536 $('.expand_commit').on('click',function(e){
536 537 var target_expand = $(this);
537 538 var cid = target_expand.data('commitId');
538 539
539 540 if (target_expand.hasClass('open')){
540 541 $('#c-'+cid).css({
541 542 'height': '1.5em',
542 543 'white-space': 'nowrap',
543 544 'text-overflow': 'ellipsis',
544 545 'overflow':'hidden'
545 546 });
546 547 target_expand.removeClass('open');
547 548 }
548 549 else {
549 550 $('#c-'+cid).css({
550 551 'height': 'auto',
551 552 'white-space': 'pre-line',
552 553 'text-overflow': 'initial',
553 554 'overflow':'visible'
554 555 });
555 556 target_expand.addClass('open');
556 557 }
557 558 });
558 559 </script>
559 560
560 561 % endif
561 562
562 563 % else:
563 564 <%include file="/compare/compare_commits.mako" />
564 565 % endif
565 566
566 567 <div class="cs_files">
567 568 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
568 569 ${cbdiffs.render_diffset_menu()}
569 570 ${cbdiffs.render_diffset(
570 571 c.diffset, use_comments=True,
571 572 collapse_when_files_over=30,
572 573 disable_new_comments=not c.allowed_to_comment,
573 574 deleted_files_comments=c.deleted_files_comments,
574 575 inline_comments=c.inline_comments)}
575 576 </div>
576 577 % else:
577 578 ## skipping commits we need to clear the view for missing commits
578 579 <div style="clear:both;"></div>
579 580 % endif
580 581
581 582 </div>
582 583 </div>
583 584
584 585 ## template for inline comment form
585 586 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
586 587
587 588 ## render general comments
588 589
589 590 <div id="comment-tr-show">
590 591 <div class="comment">
591 592 % if general_outdated_comm_count_ver:
592 593 <div class="meta">
593 594 % if general_outdated_comm_count_ver == 1:
594 595 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
595 596 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
596 597 % else:
597 598 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
598 599 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
599 600 % endif
600 601 </div>
601 602 % endif
602 603 </div>
603 604 </div>
604 605
605 606 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
606 607
607 608 % if not c.pull_request.is_closed():
608 609 ## merge status, and merge action
609 610 <div class="pull-request-merge">
610 611 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
611 612 </div>
612 613
613 614 ## main comment form and it status
614 615 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
615 616 pull_request_id=c.pull_request.pull_request_id),
616 617 c.pull_request_review_status,
617 618 is_pull_request=True, change_status=c.allowed_to_change_status)}
618 619 %endif
619 620
620 621 <script type="text/javascript">
621 622 if (location.hash) {
622 623 var result = splitDelimitedHash(location.hash);
623 624 var line = $('html').find(result.loc);
624 625 // show hidden comments if we use location.hash
625 626 if (line.hasClass('comment-general')) {
626 627 $(line).show();
627 628 } else if (line.hasClass('comment-inline')) {
628 629 $(line).show();
629 630 var $cb = $(line).closest('.cb');
630 631 $cb.removeClass('cb-collapsed')
631 632 }
632 633 if (line.length > 0){
633 634 offsetScroll(line, 70);
634 635 }
635 636 }
636 637
637 638 versionController = new VersionController();
638 639 versionController.init();
639 640
640 641 reviewersController = new ReviewersController();
641 642
642 643 $(function(){
643 644
644 645 // custom code mirror
645 646 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
646 647
647 648 var PRDetails = {
648 649 editButton: $('#open_edit_pullrequest'),
649 650 closeButton: $('#close_edit_pullrequest'),
650 651 deleteButton: $('#delete_pullrequest'),
651 652 viewFields: $('#pr-desc, #pr-title'),
652 653 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
653 654
654 655 init: function() {
655 656 var that = this;
656 657 this.editButton.on('click', function(e) { that.edit(); });
657 658 this.closeButton.on('click', function(e) { that.view(); });
658 659 },
659 660
660 661 edit: function(event) {
661 662 this.viewFields.hide();
662 663 this.editButton.hide();
663 664 this.deleteButton.hide();
664 665 this.closeButton.show();
665 666 this.editFields.show();
666 667 codeMirrorInstance.refresh();
667 668 },
668 669
669 670 view: function(event) {
670 671 this.editButton.show();
671 672 this.deleteButton.show();
672 673 this.editFields.hide();
673 674 this.closeButton.hide();
674 675 this.viewFields.show();
675 676 }
676 677 };
677 678
678 679 var ReviewersPanel = {
679 680 editButton: $('#open_edit_reviewers'),
680 681 closeButton: $('#close_edit_reviewers'),
681 682 addButton: $('#add_reviewer'),
682 683 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
683 684
684 685 init: function() {
685 686 var self = this;
686 687 this.editButton.on('click', function(e) { self.edit(); });
687 688 this.closeButton.on('click', function(e) { self.close(); });
688 689 },
689 690
690 691 edit: function(event) {
691 692 this.editButton.hide();
692 693 this.closeButton.show();
693 694 this.addButton.show();
694 695 this.removeButtons.css('visibility', 'visible');
695 696 // review rules
696 697 reviewersController.loadReviewRules(
697 698 ${c.pull_request.reviewer_data_json | n});
698 699 },
699 700
700 701 close: function(event) {
701 702 this.editButton.show();
702 703 this.closeButton.hide();
703 704 this.addButton.hide();
704 705 this.removeButtons.css('visibility', 'hidden');
705 706 // hide review rules
706 707 reviewersController.hideReviewRules()
707 708 }
708 709 };
709 710
710 711 PRDetails.init();
711 712 ReviewersPanel.init();
712 713
713 714 showOutdated = function(self){
714 715 $('.comment-inline.comment-outdated').show();
715 716 $('.filediff-outdated').show();
716 717 $('.showOutdatedComments').hide();
717 718 $('.hideOutdatedComments').show();
718 719 };
719 720
720 721 hideOutdated = function(self){
721 722 $('.comment-inline.comment-outdated').hide();
722 723 $('.filediff-outdated').hide();
723 724 $('.hideOutdatedComments').hide();
724 725 $('.showOutdatedComments').show();
725 726 };
726 727
727 728 refreshMergeChecks = function(){
728 729 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
729 730 $('.pull-request-merge').css('opacity', 0.3);
730 731 $('.action-buttons-extra').css('opacity', 0.3);
731 732
732 733 $('.pull-request-merge').load(
733 734 loadUrl, function() {
734 735 $('.pull-request-merge').css('opacity', 1);
735 736
736 737 $('.action-buttons-extra').css('opacity', 1);
737 738 injectCloseAction();
738 739 }
739 740 );
740 741 };
741 742
742 743 injectCloseAction = function() {
743 744 var closeAction = $('#close-pull-request-action').html();
744 745 var $actionButtons = $('.action-buttons-extra');
745 746 // clear the action before
746 747 $actionButtons.html("");
747 748 $actionButtons.html(closeAction);
748 749 };
749 750
750 751 closePullRequest = function (status) {
751 752 // inject closing flag
752 753 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
753 754 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
754 755 $(generalCommentForm.submitForm).submit();
755 756 };
756 757
757 758 $('#show-outdated-comments').on('click', function(e){
758 759 var button = $(this);
759 760 var outdated = $('.comment-outdated');
760 761
761 762 if (button.html() === "(Show)") {
762 763 button.html("(Hide)");
763 764 outdated.show();
764 765 } else {
765 766 button.html("(Show)");
766 767 outdated.hide();
767 768 }
768 769 });
769 770
770 771 $('.show-inline-comments').on('change', function(e){
771 772 var show = 'none';
772 773 var target = e.currentTarget;
773 774 if(target.checked){
774 775 show = ''
775 776 }
776 777 var boxid = $(target).attr('id_for');
777 778 var comments = $('#{0} .inline-comments'.format(boxid));
778 779 var fn_display = function(idx){
779 780 $(this).css('display', show);
780 781 };
781 782 $(comments).each(fn_display);
782 783 var btns = $('#{0} .inline-comments-button'.format(boxid));
783 784 $(btns).each(fn_display);
784 785 });
785 786
786 787 $('#merge_pull_request_form').submit(function() {
787 788 if (!$('#merge_pull_request').attr('disabled')) {
788 789 $('#merge_pull_request').attr('disabled', 'disabled');
789 790 }
790 791 return true;
791 792 });
792 793
793 794 $('#edit_pull_request').on('click', function(e){
794 795 var title = $('#pr-title-input').val();
795 796 var description = codeMirrorInstance.getValue();
796 797 editPullRequest(
797 798 "${c.repo_name}", "${c.pull_request.pull_request_id}",
798 799 title, description);
799 800 });
800 801
801 802 $('#update_pull_request').on('click', function(e){
802 803 $(this).attr('disabled', 'disabled');
803 804 $(this).addClass('disabled');
804 805 $(this).html(_gettext('Saving...'));
805 806 reviewersController.updateReviewers(
806 807 "${c.repo_name}", "${c.pull_request.pull_request_id}");
807 808 });
808 809
809 810 $('#update_commits').on('click', function(e){
810 811 var isDisabled = !$(e.currentTarget).attr('disabled');
811 812 $(e.currentTarget).attr('disabled', 'disabled');
812 813 $(e.currentTarget).addClass('disabled');
813 814 $(e.currentTarget).removeClass('btn-primary');
814 815 $(e.currentTarget).text(_gettext('Updating...'));
815 816 if(isDisabled){
816 817 updateCommits(
817 818 "${c.repo_name}", "${c.pull_request.pull_request_id}");
818 819 }
819 820 });
820 821 // fixing issue with caches on firefox
821 822 $('#update_commits').removeAttr("disabled");
822 823
823 824 $('.show-inline-comments').on('click', function(e){
824 825 var boxid = $(this).attr('data-comment-id');
825 826 var button = $(this);
826 827
827 828 if(button.hasClass("comments-visible")) {
828 829 $('#{0} .inline-comments'.format(boxid)).each(function(index){
829 830 $(this).hide();
830 831 });
831 832 button.removeClass("comments-visible");
832 833 } else {
833 834 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 835 $(this).show();
835 836 });
836 837 button.addClass("comments-visible");
837 838 }
838 839 });
839 840
840 841 // register submit callback on commentForm form to track TODOs
841 842 window.commentFormGlobalSubmitSuccessCallback = function(){
842 843 refreshMergeChecks();
843 844 };
844 845 // initial injection
845 846 injectCloseAction();
846 847
847 848 ReviewerAutoComplete('#user');
848 849
849 850 })
850 851 </script>
851 852
852 853 </div>
853 854 </div>
854 855
855 856 </%def>
General Comments 0
You need to be logged in to leave comments. Login now