##// END OF EJS Templates
pull-requests: allow super-admins to force change state of locked PRs....
marcink -
r4233:95c3d867 stable
parent child Browse files
Show More

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

@@ -1,1479 +1,1493 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 # backward compat., we use for OLD PRs a plain renderer
64 64 c.renderer = 'plain'
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.pull_request_state,
112 112 pr.work_in_progress, pr.target_repo.repo_name),
113 113 'name_raw': pr.pull_request_id,
114 114 'status': _render('pullrequest_status',
115 115 pr.calculated_review_status()),
116 116 'title': _render('pullrequest_title', pr.title, pr.description),
117 117 'description': h.escape(pr.description),
118 118 'updated_on': _render('pullrequest_updated_on',
119 119 h.datetime_to_time(pr.updated_on)),
120 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 121 'created_on': _render('pullrequest_updated_on',
122 122 h.datetime_to_time(pr.created_on)),
123 123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 124 'state': pr.pull_request_state,
125 125 'author': _render('pullrequest_author',
126 126 pr.author.full_contact, ),
127 127 'author_raw': pr.author.full_name,
128 128 'comments': _render('pullrequest_comments', len(comments)),
129 129 'comments_raw': len(comments),
130 130 'closed': pr.is_closed(),
131 131 })
132 132
133 133 data = ({
134 134 'draw': draw,
135 135 'data': data,
136 136 'recordsTotal': pull_requests_total_count,
137 137 'recordsFiltered': pull_requests_total_count,
138 138 })
139 139 return data
140 140
141 141 @LoginRequired()
142 142 @HasRepoPermissionAnyDecorator(
143 143 'repository.read', 'repository.write', 'repository.admin')
144 144 @view_config(
145 145 route_name='pullrequest_show_all', request_method='GET',
146 146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 147 def pull_request_list(self):
148 148 c = self.load_default_context()
149 149
150 150 req_get = self.request.GET
151 151 c.source = str2bool(req_get.get('source'))
152 152 c.closed = str2bool(req_get.get('closed'))
153 153 c.my = str2bool(req_get.get('my'))
154 154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 156
157 157 c.active = 'open'
158 158 if c.my:
159 159 c.active = 'my'
160 160 if c.closed:
161 161 c.active = 'closed'
162 162 if c.awaiting_review and not c.source:
163 163 c.active = 'awaiting'
164 164 if c.source and not c.awaiting_review:
165 165 c.active = 'source'
166 166 if c.awaiting_my_review:
167 167 c.active = 'awaiting_my'
168 168
169 169 return self._get_template_context(c)
170 170
171 171 @LoginRequired()
172 172 @HasRepoPermissionAnyDecorator(
173 173 'repository.read', 'repository.write', 'repository.admin')
174 174 @view_config(
175 175 route_name='pullrequest_show_all_data', request_method='GET',
176 176 renderer='json_ext', xhr=True)
177 177 def pull_request_list_data(self):
178 178 self.load_default_context()
179 179
180 180 # additional filters
181 181 req_get = self.request.GET
182 182 source = str2bool(req_get.get('source'))
183 183 closed = str2bool(req_get.get('closed'))
184 184 my = str2bool(req_get.get('my'))
185 185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 187
188 188 filter_type = 'awaiting_review' if awaiting_review \
189 189 else 'awaiting_my_review' if awaiting_my_review \
190 190 else None
191 191
192 192 opened_by = None
193 193 if my:
194 194 opened_by = [self._rhodecode_user.user_id]
195 195
196 196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 197 if closed:
198 198 statuses = [PullRequest.STATUS_CLOSED]
199 199
200 200 data = self._get_pull_requests_list(
201 201 repo_name=self.db_repo_name, source=source,
202 202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 203
204 204 return data
205 205
206 206 def _is_diff_cache_enabled(self, target_repo):
207 207 caching_enabled = self._get_general_setting(
208 208 target_repo, 'rhodecode_diff_cache')
209 209 log.debug('Diff caching enabled: %s', caching_enabled)
210 210 return caching_enabled
211 211
212 212 def _get_diffset(self, source_repo_name, source_repo,
213 213 source_ref_id, target_ref_id,
214 214 target_commit, source_commit, diff_limit, file_limit,
215 215 fulldiff, hide_whitespace_changes, diff_context):
216 216
217 217 vcs_diff = PullRequestModel().get_diff(
218 218 source_repo, source_ref_id, target_ref_id,
219 219 hide_whitespace_changes, diff_context)
220 220
221 221 diff_processor = diffs.DiffProcessor(
222 222 vcs_diff, format='newdiff', diff_limit=diff_limit,
223 223 file_limit=file_limit, show_full_diff=fulldiff)
224 224
225 225 _parsed = diff_processor.prepare()
226 226
227 227 diffset = codeblocks.DiffSet(
228 228 repo_name=self.db_repo_name,
229 229 source_repo_name=source_repo_name,
230 230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
231 231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
232 232 )
233 233 diffset = self.path_filter.render_patchset_filtered(
234 234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
235 235
236 236 return diffset
237 237
238 238 def _get_range_diffset(self, source_scm, source_repo,
239 239 commit1, commit2, diff_limit, file_limit,
240 240 fulldiff, hide_whitespace_changes, diff_context):
241 241 vcs_diff = source_scm.get_diff(
242 242 commit1, commit2,
243 243 ignore_whitespace=hide_whitespace_changes,
244 244 context=diff_context)
245 245
246 246 diff_processor = diffs.DiffProcessor(
247 247 vcs_diff, format='newdiff', diff_limit=diff_limit,
248 248 file_limit=file_limit, show_full_diff=fulldiff)
249 249
250 250 _parsed = diff_processor.prepare()
251 251
252 252 diffset = codeblocks.DiffSet(
253 253 repo_name=source_repo.repo_name,
254 254 source_node_getter=codeblocks.diffset_node_getter(commit1),
255 255 target_node_getter=codeblocks.diffset_node_getter(commit2))
256 256
257 257 diffset = self.path_filter.render_patchset_filtered(
258 258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
259 259
260 260 return diffset
261 261
262 262 @LoginRequired()
263 263 @HasRepoPermissionAnyDecorator(
264 264 'repository.read', 'repository.write', 'repository.admin')
265 265 @view_config(
266 266 route_name='pullrequest_show', request_method='GET',
267 267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 268 def pull_request_show(self):
269 269 _ = self.request.translate
270 270 c = self.load_default_context()
271 271
272 272 pull_request = PullRequest.get_or_404(
273 273 self.request.matchdict['pull_request_id'])
274 274 pull_request_id = pull_request.pull_request_id
275 275
276 276 c.state_progressing = pull_request.is_state_changing()
277 277
278 _new_state = {
279 'created': PullRequest.STATE_CREATED,
280 }.get(self.request.GET.get('force_state'))
281 if c.is_super_admin and _new_state:
282 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
283 h.flash(
284 _('Pull Request state was force changed to `{}`').format(_new_state),
285 category='success')
286 Session().commit()
287
288 raise HTTPFound(h.route_path(
289 'pullrequest_show', repo_name=self.db_repo_name,
290 pull_request_id=pull_request_id))
291
278 292 version = self.request.GET.get('version')
279 293 from_version = self.request.GET.get('from_version') or version
280 294 merge_checks = self.request.GET.get('merge_checks')
281 295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
282 296
283 297 # fetch global flags of ignore ws or context lines
284 298 diff_context = diffs.get_diff_context(self.request)
285 299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
286 300
287 301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
288 302
289 303 (pull_request_latest,
290 304 pull_request_at_ver,
291 305 pull_request_display_obj,
292 306 at_version) = PullRequestModel().get_pr_version(
293 307 pull_request_id, version=version)
294 308 pr_closed = pull_request_latest.is_closed()
295 309
296 310 if pr_closed and (version or from_version):
297 311 # not allow to browse versions
298 312 raise HTTPFound(h.route_path(
299 313 'pullrequest_show', repo_name=self.db_repo_name,
300 314 pull_request_id=pull_request_id))
301 315
302 316 versions = pull_request_display_obj.versions()
303 317 # used to store per-commit range diffs
304 318 c.changes = collections.OrderedDict()
305 319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
306 320
307 321 c.at_version = at_version
308 322 c.at_version_num = (at_version
309 323 if at_version and at_version != 'latest'
310 324 else None)
311 325 c.at_version_pos = ChangesetComment.get_index_from_version(
312 326 c.at_version_num, versions)
313 327
314 328 (prev_pull_request_latest,
315 329 prev_pull_request_at_ver,
316 330 prev_pull_request_display_obj,
317 331 prev_at_version) = PullRequestModel().get_pr_version(
318 332 pull_request_id, version=from_version)
319 333
320 334 c.from_version = prev_at_version
321 335 c.from_version_num = (prev_at_version
322 336 if prev_at_version and prev_at_version != 'latest'
323 337 else None)
324 338 c.from_version_pos = ChangesetComment.get_index_from_version(
325 339 c.from_version_num, versions)
326 340
327 341 # define if we're in COMPARE mode or VIEW at version mode
328 342 compare = at_version != prev_at_version
329 343
330 344 # pull_requests repo_name we opened it against
331 345 # ie. target_repo must match
332 346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
333 347 raise HTTPNotFound()
334 348
335 349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
336 350 pull_request_at_ver)
337 351
338 352 c.pull_request = pull_request_display_obj
339 353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
340 354 c.pull_request_latest = pull_request_latest
341 355
342 356 if compare or (at_version and not at_version == 'latest'):
343 357 c.allowed_to_change_status = False
344 358 c.allowed_to_update = False
345 359 c.allowed_to_merge = False
346 360 c.allowed_to_delete = False
347 361 c.allowed_to_comment = False
348 362 c.allowed_to_close = False
349 363 else:
350 364 can_change_status = PullRequestModel().check_user_change_status(
351 365 pull_request_at_ver, self._rhodecode_user)
352 366 c.allowed_to_change_status = can_change_status and not pr_closed
353 367
354 368 c.allowed_to_update = PullRequestModel().check_user_update(
355 369 pull_request_latest, self._rhodecode_user) and not pr_closed
356 370 c.allowed_to_merge = PullRequestModel().check_user_merge(
357 371 pull_request_latest, self._rhodecode_user) and not pr_closed
358 372 c.allowed_to_delete = PullRequestModel().check_user_delete(
359 373 pull_request_latest, self._rhodecode_user) and not pr_closed
360 374 c.allowed_to_comment = not pr_closed
361 375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
362 376
363 377 c.forbid_adding_reviewers = False
364 378 c.forbid_author_to_review = False
365 379 c.forbid_commit_author_to_review = False
366 380
367 381 if pull_request_latest.reviewer_data and \
368 382 'rules' in pull_request_latest.reviewer_data:
369 383 rules = pull_request_latest.reviewer_data['rules'] or {}
370 384 try:
371 385 c.forbid_adding_reviewers = rules.get(
372 386 'forbid_adding_reviewers')
373 387 c.forbid_author_to_review = rules.get(
374 388 'forbid_author_to_review')
375 389 c.forbid_commit_author_to_review = rules.get(
376 390 'forbid_commit_author_to_review')
377 391 except Exception:
378 392 pass
379 393
380 394 # check merge capabilities
381 395 _merge_check = MergeCheck.validate(
382 396 pull_request_latest, auth_user=self._rhodecode_user,
383 397 translator=self.request.translate,
384 398 force_shadow_repo_refresh=force_refresh)
385 399 c.pr_merge_errors = _merge_check.error_details
386 400 c.pr_merge_possible = not _merge_check.failed
387 401 c.pr_merge_message = _merge_check.merge_msg
388 402
389 403 c.pr_merge_info = MergeCheck.get_merge_conditions(
390 404 pull_request_latest, translator=self.request.translate)
391 405
392 406 c.pull_request_review_status = _merge_check.review_status
393 407 if merge_checks:
394 408 self.request.override_renderer = \
395 409 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
396 410 return self._get_template_context(c)
397 411
398 412 comments_model = CommentsModel()
399 413
400 414 # reviewers and statuses
401 415 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
402 416 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
403 417
404 418 # GENERAL COMMENTS with versions #
405 419 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
406 420 q = q.order_by(ChangesetComment.comment_id.asc())
407 421 general_comments = q
408 422
409 423 # pick comments we want to render at current version
410 424 c.comment_versions = comments_model.aggregate_comments(
411 425 general_comments, versions, c.at_version_num)
412 426 c.comments = c.comment_versions[c.at_version_num]['until']
413 427
414 428 # INLINE COMMENTS with versions #
415 429 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
416 430 q = q.order_by(ChangesetComment.comment_id.asc())
417 431 inline_comments = q
418 432
419 433 c.inline_versions = comments_model.aggregate_comments(
420 434 inline_comments, versions, c.at_version_num, inline=True)
421 435
422 436 # TODOs
423 437 c.unresolved_comments = CommentsModel() \
424 438 .get_pull_request_unresolved_todos(pull_request)
425 439 c.resolved_comments = CommentsModel() \
426 440 .get_pull_request_resolved_todos(pull_request)
427 441
428 442 # inject latest version
429 443 latest_ver = PullRequest.get_pr_display_object(
430 444 pull_request_latest, pull_request_latest)
431 445
432 446 c.versions = versions + [latest_ver]
433 447
434 448 # if we use version, then do not show later comments
435 449 # than current version
436 450 display_inline_comments = collections.defaultdict(
437 451 lambda: collections.defaultdict(list))
438 452 for co in inline_comments:
439 453 if c.at_version_num:
440 454 # pick comments that are at least UPTO given version, so we
441 455 # don't render comments for higher version
442 456 should_render = co.pull_request_version_id and \
443 457 co.pull_request_version_id <= c.at_version_num
444 458 else:
445 459 # showing all, for 'latest'
446 460 should_render = True
447 461
448 462 if should_render:
449 463 display_inline_comments[co.f_path][co.line_no].append(co)
450 464
451 465 # load diff data into template context, if we use compare mode then
452 466 # diff is calculated based on changes between versions of PR
453 467
454 468 source_repo = pull_request_at_ver.source_repo
455 469 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
456 470
457 471 target_repo = pull_request_at_ver.target_repo
458 472 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
459 473
460 474 if compare:
461 475 # in compare switch the diff base to latest commit from prev version
462 476 target_ref_id = prev_pull_request_display_obj.revisions[0]
463 477
464 478 # despite opening commits for bookmarks/branches/tags, we always
465 479 # convert this to rev to prevent changes after bookmark or branch change
466 480 c.source_ref_type = 'rev'
467 481 c.source_ref = source_ref_id
468 482
469 483 c.target_ref_type = 'rev'
470 484 c.target_ref = target_ref_id
471 485
472 486 c.source_repo = source_repo
473 487 c.target_repo = target_repo
474 488
475 489 c.commit_ranges = []
476 490 source_commit = EmptyCommit()
477 491 target_commit = EmptyCommit()
478 492 c.missing_requirements = False
479 493
480 494 source_scm = source_repo.scm_instance()
481 495 target_scm = target_repo.scm_instance()
482 496
483 497 shadow_scm = None
484 498 try:
485 499 shadow_scm = pull_request_latest.get_shadow_repo()
486 500 except Exception:
487 501 log.debug('Failed to get shadow repo', exc_info=True)
488 502 # try first the existing source_repo, and then shadow
489 503 # repo if we can obtain one
490 504 commits_source_repo = source_scm
491 505 if shadow_scm:
492 506 commits_source_repo = shadow_scm
493 507
494 508 c.commits_source_repo = commits_source_repo
495 509 c.ancestor = None # set it to None, to hide it from PR view
496 510
497 511 # empty version means latest, so we keep this to prevent
498 512 # double caching
499 513 version_normalized = version or 'latest'
500 514 from_version_normalized = from_version or 'latest'
501 515
502 516 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
503 517 cache_file_path = diff_cache_exist(
504 518 cache_path, 'pull_request', pull_request_id, version_normalized,
505 519 from_version_normalized, source_ref_id, target_ref_id,
506 520 hide_whitespace_changes, diff_context, c.fulldiff)
507 521
508 522 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
509 523 force_recache = self.get_recache_flag()
510 524
511 525 cached_diff = None
512 526 if caching_enabled:
513 527 cached_diff = load_cached_diff(cache_file_path)
514 528
515 529 has_proper_commit_cache = (
516 530 cached_diff and cached_diff.get('commits')
517 531 and len(cached_diff.get('commits', [])) == 5
518 532 and cached_diff.get('commits')[0]
519 533 and cached_diff.get('commits')[3])
520 534
521 535 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
522 536 diff_commit_cache = \
523 537 (ancestor_commit, commit_cache, missing_requirements,
524 538 source_commit, target_commit) = cached_diff['commits']
525 539 else:
526 540 diff_commit_cache = \
527 541 (ancestor_commit, commit_cache, missing_requirements,
528 542 source_commit, target_commit) = self.get_commits(
529 543 commits_source_repo,
530 544 pull_request_at_ver,
531 545 source_commit,
532 546 source_ref_id,
533 547 source_scm,
534 548 target_commit,
535 549 target_ref_id,
536 550 target_scm)
537 551
538 552 # register our commit range
539 553 for comm in commit_cache.values():
540 554 c.commit_ranges.append(comm)
541 555
542 556 c.missing_requirements = missing_requirements
543 557 c.ancestor_commit = ancestor_commit
544 558 c.statuses = source_repo.statuses(
545 559 [x.raw_id for x in c.commit_ranges])
546 560
547 561 # auto collapse if we have more than limit
548 562 collapse_limit = diffs.DiffProcessor._collapse_commits_over
549 563 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
550 564 c.compare_mode = compare
551 565
552 566 # diff_limit is the old behavior, will cut off the whole diff
553 567 # if the limit is applied otherwise will just hide the
554 568 # big files from the front-end
555 569 diff_limit = c.visual.cut_off_limit_diff
556 570 file_limit = c.visual.cut_off_limit_file
557 571
558 572 c.missing_commits = False
559 573 if (c.missing_requirements
560 574 or isinstance(source_commit, EmptyCommit)
561 575 or source_commit == target_commit):
562 576
563 577 c.missing_commits = True
564 578 else:
565 579 c.inline_comments = display_inline_comments
566 580
567 581 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
568 582 if not force_recache and has_proper_diff_cache:
569 583 c.diffset = cached_diff['diff']
570 584 (ancestor_commit, commit_cache, missing_requirements,
571 585 source_commit, target_commit) = cached_diff['commits']
572 586 else:
573 587 c.diffset = self._get_diffset(
574 588 c.source_repo.repo_name, commits_source_repo,
575 589 source_ref_id, target_ref_id,
576 590 target_commit, source_commit,
577 591 diff_limit, file_limit, c.fulldiff,
578 592 hide_whitespace_changes, diff_context)
579 593
580 594 # save cached diff
581 595 if caching_enabled:
582 596 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
583 597
584 598 c.limited_diff = c.diffset.limited_diff
585 599
586 600 # calculate removed files that are bound to comments
587 601 comment_deleted_files = [
588 602 fname for fname in display_inline_comments
589 603 if fname not in c.diffset.file_stats]
590 604
591 605 c.deleted_files_comments = collections.defaultdict(dict)
592 606 for fname, per_line_comments in display_inline_comments.items():
593 607 if fname in comment_deleted_files:
594 608 c.deleted_files_comments[fname]['stats'] = 0
595 609 c.deleted_files_comments[fname]['comments'] = list()
596 610 for lno, comments in per_line_comments.items():
597 611 c.deleted_files_comments[fname]['comments'].extend(comments)
598 612
599 613 # maybe calculate the range diff
600 614 if c.range_diff_on:
601 615 # TODO(marcink): set whitespace/context
602 616 context_lcl = 3
603 617 ign_whitespace_lcl = False
604 618
605 619 for commit in c.commit_ranges:
606 620 commit2 = commit
607 621 commit1 = commit.first_parent
608 622
609 623 range_diff_cache_file_path = diff_cache_exist(
610 624 cache_path, 'diff', commit.raw_id,
611 625 ign_whitespace_lcl, context_lcl, c.fulldiff)
612 626
613 627 cached_diff = None
614 628 if caching_enabled:
615 629 cached_diff = load_cached_diff(range_diff_cache_file_path)
616 630
617 631 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
618 632 if not force_recache and has_proper_diff_cache:
619 633 diffset = cached_diff['diff']
620 634 else:
621 635 diffset = self._get_range_diffset(
622 636 commits_source_repo, source_repo,
623 637 commit1, commit2, diff_limit, file_limit,
624 638 c.fulldiff, ign_whitespace_lcl, context_lcl
625 639 )
626 640
627 641 # save cached diff
628 642 if caching_enabled:
629 643 cache_diff(range_diff_cache_file_path, diffset, None)
630 644
631 645 c.changes[commit.raw_id] = diffset
632 646
633 647 # this is a hack to properly display links, when creating PR, the
634 648 # compare view and others uses different notation, and
635 649 # compare_commits.mako renders links based on the target_repo.
636 650 # We need to swap that here to generate it properly on the html side
637 651 c.target_repo = c.source_repo
638 652
639 653 c.commit_statuses = ChangesetStatus.STATUSES
640 654
641 655 c.show_version_changes = not pr_closed
642 656 if c.show_version_changes:
643 657 cur_obj = pull_request_at_ver
644 658 prev_obj = prev_pull_request_at_ver
645 659
646 660 old_commit_ids = prev_obj.revisions
647 661 new_commit_ids = cur_obj.revisions
648 662 commit_changes = PullRequestModel()._calculate_commit_id_changes(
649 663 old_commit_ids, new_commit_ids)
650 664 c.commit_changes_summary = commit_changes
651 665
652 666 # calculate the diff for commits between versions
653 667 c.commit_changes = []
654 668 mark = lambda cs, fw: list(
655 669 h.itertools.izip_longest([], cs, fillvalue=fw))
656 670 for c_type, raw_id in mark(commit_changes.added, 'a') \
657 671 + mark(commit_changes.removed, 'r') \
658 672 + mark(commit_changes.common, 'c'):
659 673
660 674 if raw_id in commit_cache:
661 675 commit = commit_cache[raw_id]
662 676 else:
663 677 try:
664 678 commit = commits_source_repo.get_commit(raw_id)
665 679 except CommitDoesNotExistError:
666 680 # in case we fail extracting still use "dummy" commit
667 681 # for display in commit diff
668 682 commit = h.AttributeDict(
669 683 {'raw_id': raw_id,
670 684 'message': 'EMPTY or MISSING COMMIT'})
671 685 c.commit_changes.append([c_type, commit])
672 686
673 687 # current user review statuses for each version
674 688 c.review_versions = {}
675 689 if self._rhodecode_user.user_id in allowed_reviewers:
676 690 for co in general_comments:
677 691 if co.author.user_id == self._rhodecode_user.user_id:
678 692 status = co.status_change
679 693 if status:
680 694 _ver_pr = status[0].comment.pull_request_version_id
681 695 c.review_versions[_ver_pr] = status[0]
682 696
683 697 return self._get_template_context(c)
684 698
685 699 def get_commits(
686 700 self, commits_source_repo, pull_request_at_ver, source_commit,
687 701 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
688 702 commit_cache = collections.OrderedDict()
689 703 missing_requirements = False
690 704 try:
691 705 pre_load = ["author", "date", "message", "branch", "parents"]
692 706 show_revs = pull_request_at_ver.revisions
693 707 for rev in show_revs:
694 708 comm = commits_source_repo.get_commit(
695 709 commit_id=rev, pre_load=pre_load)
696 710 commit_cache[comm.raw_id] = comm
697 711
698 712 # Order here matters, we first need to get target, and then
699 713 # the source
700 714 target_commit = commits_source_repo.get_commit(
701 715 commit_id=safe_str(target_ref_id))
702 716
703 717 source_commit = commits_source_repo.get_commit(
704 718 commit_id=safe_str(source_ref_id))
705 719 except CommitDoesNotExistError:
706 720 log.warning(
707 721 'Failed to get commit from `{}` repo'.format(
708 722 commits_source_repo), exc_info=True)
709 723 except RepositoryRequirementError:
710 724 log.warning(
711 725 'Failed to get all required data from repo', exc_info=True)
712 726 missing_requirements = True
713 727 ancestor_commit = None
714 728 try:
715 729 ancestor_id = source_scm.get_common_ancestor(
716 730 source_commit.raw_id, target_commit.raw_id, target_scm)
717 731 ancestor_commit = source_scm.get_commit(ancestor_id)
718 732 except Exception:
719 733 ancestor_commit = None
720 734 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
721 735
722 736 def assure_not_empty_repo(self):
723 737 _ = self.request.translate
724 738
725 739 try:
726 740 self.db_repo.scm_instance().get_commit()
727 741 except EmptyRepositoryError:
728 742 h.flash(h.literal(_('There are no commits yet')),
729 743 category='warning')
730 744 raise HTTPFound(
731 745 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
732 746
733 747 @LoginRequired()
734 748 @NotAnonymous()
735 749 @HasRepoPermissionAnyDecorator(
736 750 'repository.read', 'repository.write', 'repository.admin')
737 751 @view_config(
738 752 route_name='pullrequest_new', request_method='GET',
739 753 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
740 754 def pull_request_new(self):
741 755 _ = self.request.translate
742 756 c = self.load_default_context()
743 757
744 758 self.assure_not_empty_repo()
745 759 source_repo = self.db_repo
746 760
747 761 commit_id = self.request.GET.get('commit')
748 762 branch_ref = self.request.GET.get('branch')
749 763 bookmark_ref = self.request.GET.get('bookmark')
750 764
751 765 try:
752 766 source_repo_data = PullRequestModel().generate_repo_data(
753 767 source_repo, commit_id=commit_id,
754 768 branch=branch_ref, bookmark=bookmark_ref,
755 769 translator=self.request.translate)
756 770 except CommitDoesNotExistError as e:
757 771 log.exception(e)
758 772 h.flash(_('Commit does not exist'), 'error')
759 773 raise HTTPFound(
760 774 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
761 775
762 776 default_target_repo = source_repo
763 777
764 778 if source_repo.parent and c.has_origin_repo_read_perm:
765 779 parent_vcs_obj = source_repo.parent.scm_instance()
766 780 if parent_vcs_obj and not parent_vcs_obj.is_empty():
767 781 # change default if we have a parent repo
768 782 default_target_repo = source_repo.parent
769 783
770 784 target_repo_data = PullRequestModel().generate_repo_data(
771 785 default_target_repo, translator=self.request.translate)
772 786
773 787 selected_source_ref = source_repo_data['refs']['selected_ref']
774 788 title_source_ref = ''
775 789 if selected_source_ref:
776 790 title_source_ref = selected_source_ref.split(':', 2)[1]
777 791 c.default_title = PullRequestModel().generate_pullrequest_title(
778 792 source=source_repo.repo_name,
779 793 source_ref=title_source_ref,
780 794 target=default_target_repo.repo_name
781 795 )
782 796
783 797 c.default_repo_data = {
784 798 'source_repo_name': source_repo.repo_name,
785 799 'source_refs_json': json.dumps(source_repo_data),
786 800 'target_repo_name': default_target_repo.repo_name,
787 801 'target_refs_json': json.dumps(target_repo_data),
788 802 }
789 803 c.default_source_ref = selected_source_ref
790 804
791 805 return self._get_template_context(c)
792 806
793 807 @LoginRequired()
794 808 @NotAnonymous()
795 809 @HasRepoPermissionAnyDecorator(
796 810 'repository.read', 'repository.write', 'repository.admin')
797 811 @view_config(
798 812 route_name='pullrequest_repo_refs', request_method='GET',
799 813 renderer='json_ext', xhr=True)
800 814 def pull_request_repo_refs(self):
801 815 self.load_default_context()
802 816 target_repo_name = self.request.matchdict['target_repo_name']
803 817 repo = Repository.get_by_repo_name(target_repo_name)
804 818 if not repo:
805 819 raise HTTPNotFound()
806 820
807 821 target_perm = HasRepoPermissionAny(
808 822 'repository.read', 'repository.write', 'repository.admin')(
809 823 target_repo_name)
810 824 if not target_perm:
811 825 raise HTTPNotFound()
812 826
813 827 return PullRequestModel().generate_repo_data(
814 828 repo, translator=self.request.translate)
815 829
816 830 @LoginRequired()
817 831 @NotAnonymous()
818 832 @HasRepoPermissionAnyDecorator(
819 833 'repository.read', 'repository.write', 'repository.admin')
820 834 @view_config(
821 835 route_name='pullrequest_repo_targets', request_method='GET',
822 836 renderer='json_ext', xhr=True)
823 837 def pullrequest_repo_targets(self):
824 838 _ = self.request.translate
825 839 filter_query = self.request.GET.get('query')
826 840
827 841 # get the parents
828 842 parent_target_repos = []
829 843 if self.db_repo.parent:
830 844 parents_query = Repository.query() \
831 845 .order_by(func.length(Repository.repo_name)) \
832 846 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
833 847
834 848 if filter_query:
835 849 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
836 850 parents_query = parents_query.filter(
837 851 Repository.repo_name.ilike(ilike_expression))
838 852 parents = parents_query.limit(20).all()
839 853
840 854 for parent in parents:
841 855 parent_vcs_obj = parent.scm_instance()
842 856 if parent_vcs_obj and not parent_vcs_obj.is_empty():
843 857 parent_target_repos.append(parent)
844 858
845 859 # get other forks, and repo itself
846 860 query = Repository.query() \
847 861 .order_by(func.length(Repository.repo_name)) \
848 862 .filter(
849 863 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
850 864 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
851 865 ) \
852 866 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
853 867
854 868 if filter_query:
855 869 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
856 870 query = query.filter(Repository.repo_name.ilike(ilike_expression))
857 871
858 872 limit = max(20 - len(parent_target_repos), 5) # not less then 5
859 873 target_repos = query.limit(limit).all()
860 874
861 875 all_target_repos = target_repos + parent_target_repos
862 876
863 877 repos = []
864 878 # This checks permissions to the repositories
865 879 for obj in ScmModel().get_repos(all_target_repos):
866 880 repos.append({
867 881 'id': obj['name'],
868 882 'text': obj['name'],
869 883 'type': 'repo',
870 884 'repo_id': obj['dbrepo']['repo_id'],
871 885 'repo_type': obj['dbrepo']['repo_type'],
872 886 'private': obj['dbrepo']['private'],
873 887
874 888 })
875 889
876 890 data = {
877 891 'more': False,
878 892 'results': [{
879 893 'text': _('Repositories'),
880 894 'children': repos
881 895 }] if repos else []
882 896 }
883 897 return data
884 898
885 899 @LoginRequired()
886 900 @NotAnonymous()
887 901 @HasRepoPermissionAnyDecorator(
888 902 'repository.read', 'repository.write', 'repository.admin')
889 903 @CSRFRequired()
890 904 @view_config(
891 905 route_name='pullrequest_create', request_method='POST',
892 906 renderer=None)
893 907 def pull_request_create(self):
894 908 _ = self.request.translate
895 909 self.assure_not_empty_repo()
896 910 self.load_default_context()
897 911
898 912 controls = peppercorn.parse(self.request.POST.items())
899 913
900 914 try:
901 915 form = PullRequestForm(
902 916 self.request.translate, self.db_repo.repo_id)()
903 917 _form = form.to_python(controls)
904 918 except formencode.Invalid as errors:
905 919 if errors.error_dict.get('revisions'):
906 920 msg = 'Revisions: %s' % errors.error_dict['revisions']
907 921 elif errors.error_dict.get('pullrequest_title'):
908 922 msg = errors.error_dict.get('pullrequest_title')
909 923 else:
910 924 msg = _('Error creating pull request: {}').format(errors)
911 925 log.exception(msg)
912 926 h.flash(msg, 'error')
913 927
914 928 # would rather just go back to form ...
915 929 raise HTTPFound(
916 930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
917 931
918 932 source_repo = _form['source_repo']
919 933 source_ref = _form['source_ref']
920 934 target_repo = _form['target_repo']
921 935 target_ref = _form['target_ref']
922 936 commit_ids = _form['revisions'][::-1]
923 937
924 938 # find the ancestor for this pr
925 939 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
926 940 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
927 941
928 942 if not (source_db_repo or target_db_repo):
929 943 h.flash(_('source_repo or target repo not found'), category='error')
930 944 raise HTTPFound(
931 945 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
932 946
933 947 # re-check permissions again here
934 948 # source_repo we must have read permissions
935 949
936 950 source_perm = HasRepoPermissionAny(
937 951 'repository.read', 'repository.write', 'repository.admin')(
938 952 source_db_repo.repo_name)
939 953 if not source_perm:
940 954 msg = _('Not Enough permissions to source repo `{}`.'.format(
941 955 source_db_repo.repo_name))
942 956 h.flash(msg, category='error')
943 957 # copy the args back to redirect
944 958 org_query = self.request.GET.mixed()
945 959 raise HTTPFound(
946 960 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
947 961 _query=org_query))
948 962
949 963 # target repo we must have read permissions, and also later on
950 964 # we want to check branch permissions here
951 965 target_perm = HasRepoPermissionAny(
952 966 'repository.read', 'repository.write', 'repository.admin')(
953 967 target_db_repo.repo_name)
954 968 if not target_perm:
955 969 msg = _('Not Enough permissions to target repo `{}`.'.format(
956 970 target_db_repo.repo_name))
957 971 h.flash(msg, category='error')
958 972 # copy the args back to redirect
959 973 org_query = self.request.GET.mixed()
960 974 raise HTTPFound(
961 975 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
962 976 _query=org_query))
963 977
964 978 source_scm = source_db_repo.scm_instance()
965 979 target_scm = target_db_repo.scm_instance()
966 980
967 981 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
968 982 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
969 983
970 984 ancestor = source_scm.get_common_ancestor(
971 985 source_commit.raw_id, target_commit.raw_id, target_scm)
972 986
973 987 # recalculate target ref based on ancestor
974 988 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
975 989 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
976 990
977 991 get_default_reviewers_data, validate_default_reviewers = \
978 992 PullRequestModel().get_reviewer_functions()
979 993
980 994 # recalculate reviewers logic, to make sure we can validate this
981 995 reviewer_rules = get_default_reviewers_data(
982 996 self._rhodecode_db_user, source_db_repo,
983 997 source_commit, target_db_repo, target_commit)
984 998
985 999 given_reviewers = _form['review_members']
986 1000 reviewers = validate_default_reviewers(
987 1001 given_reviewers, reviewer_rules)
988 1002
989 1003 pullrequest_title = _form['pullrequest_title']
990 1004 title_source_ref = source_ref.split(':', 2)[1]
991 1005 if not pullrequest_title:
992 1006 pullrequest_title = PullRequestModel().generate_pullrequest_title(
993 1007 source=source_repo,
994 1008 source_ref=title_source_ref,
995 1009 target=target_repo
996 1010 )
997 1011
998 1012 description = _form['pullrequest_desc']
999 1013 description_renderer = _form['description_renderer']
1000 1014
1001 1015 try:
1002 1016 pull_request = PullRequestModel().create(
1003 1017 created_by=self._rhodecode_user.user_id,
1004 1018 source_repo=source_repo,
1005 1019 source_ref=source_ref,
1006 1020 target_repo=target_repo,
1007 1021 target_ref=target_ref,
1008 1022 revisions=commit_ids,
1009 1023 reviewers=reviewers,
1010 1024 title=pullrequest_title,
1011 1025 description=description,
1012 1026 description_renderer=description_renderer,
1013 1027 reviewer_data=reviewer_rules,
1014 1028 auth_user=self._rhodecode_user
1015 1029 )
1016 1030 Session().commit()
1017 1031
1018 1032 h.flash(_('Successfully opened new pull request'),
1019 1033 category='success')
1020 1034 except Exception:
1021 1035 msg = _('Error occurred during creation of this pull request.')
1022 1036 log.exception(msg)
1023 1037 h.flash(msg, category='error')
1024 1038
1025 1039 # copy the args back to redirect
1026 1040 org_query = self.request.GET.mixed()
1027 1041 raise HTTPFound(
1028 1042 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1029 1043 _query=org_query))
1030 1044
1031 1045 raise HTTPFound(
1032 1046 h.route_path('pullrequest_show', repo_name=target_repo,
1033 1047 pull_request_id=pull_request.pull_request_id))
1034 1048
1035 1049 @LoginRequired()
1036 1050 @NotAnonymous()
1037 1051 @HasRepoPermissionAnyDecorator(
1038 1052 'repository.read', 'repository.write', 'repository.admin')
1039 1053 @CSRFRequired()
1040 1054 @view_config(
1041 1055 route_name='pullrequest_update', request_method='POST',
1042 1056 renderer='json_ext')
1043 1057 def pull_request_update(self):
1044 1058 pull_request = PullRequest.get_or_404(
1045 1059 self.request.matchdict['pull_request_id'])
1046 1060 _ = self.request.translate
1047 1061
1048 1062 self.load_default_context()
1049 1063 redirect_url = None
1050 1064
1051 1065 if pull_request.is_closed():
1052 1066 log.debug('update: forbidden because pull request is closed')
1053 1067 msg = _(u'Cannot update closed pull requests.')
1054 1068 h.flash(msg, category='error')
1055 1069 return {'response': True,
1056 1070 'redirect_url': redirect_url}
1057 1071
1058 1072 is_state_changing = pull_request.is_state_changing()
1059 1073
1060 1074 # only owner or admin can update it
1061 1075 allowed_to_update = PullRequestModel().check_user_update(
1062 1076 pull_request, self._rhodecode_user)
1063 1077 if allowed_to_update:
1064 1078 controls = peppercorn.parse(self.request.POST.items())
1065 1079 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1066 1080
1067 1081 if 'review_members' in controls:
1068 1082 self._update_reviewers(
1069 1083 pull_request, controls['review_members'],
1070 1084 pull_request.reviewer_data)
1071 1085 elif str2bool(self.request.POST.get('update_commits', 'false')):
1072 1086 if is_state_changing:
1073 1087 log.debug('commits update: forbidden because pull request is in state %s',
1074 1088 pull_request.pull_request_state)
1075 1089 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1076 1090 u'Current state is: `{}`').format(
1077 1091 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1078 1092 h.flash(msg, category='error')
1079 1093 return {'response': True,
1080 1094 'redirect_url': redirect_url}
1081 1095
1082 1096 self._update_commits(pull_request)
1083 1097 if force_refresh:
1084 1098 redirect_url = h.route_path(
1085 1099 'pullrequest_show', repo_name=self.db_repo_name,
1086 1100 pull_request_id=pull_request.pull_request_id,
1087 1101 _query={"force_refresh": 1})
1088 1102 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1089 1103 self._edit_pull_request(pull_request)
1090 1104 else:
1091 1105 raise HTTPBadRequest()
1092 1106
1093 1107 return {'response': True,
1094 1108 'redirect_url': redirect_url}
1095 1109 raise HTTPForbidden()
1096 1110
1097 1111 def _edit_pull_request(self, pull_request):
1098 1112 _ = self.request.translate
1099 1113
1100 1114 try:
1101 1115 PullRequestModel().edit(
1102 1116 pull_request,
1103 1117 self.request.POST.get('title'),
1104 1118 self.request.POST.get('description'),
1105 1119 self.request.POST.get('description_renderer'),
1106 1120 self._rhodecode_user)
1107 1121 except ValueError:
1108 1122 msg = _(u'Cannot update closed pull requests.')
1109 1123 h.flash(msg, category='error')
1110 1124 return
1111 1125 else:
1112 1126 Session().commit()
1113 1127
1114 1128 msg = _(u'Pull request title & description updated.')
1115 1129 h.flash(msg, category='success')
1116 1130 return
1117 1131
1118 1132 def _update_commits(self, pull_request):
1119 1133 _ = self.request.translate
1120 1134
1121 1135 with pull_request.set_state(PullRequest.STATE_UPDATING):
1122 1136 resp = PullRequestModel().update_commits(
1123 1137 pull_request, self._rhodecode_db_user)
1124 1138
1125 1139 if resp.executed:
1126 1140
1127 1141 if resp.target_changed and resp.source_changed:
1128 1142 changed = 'target and source repositories'
1129 1143 elif resp.target_changed and not resp.source_changed:
1130 1144 changed = 'target repository'
1131 1145 elif not resp.target_changed and resp.source_changed:
1132 1146 changed = 'source repository'
1133 1147 else:
1134 1148 changed = 'nothing'
1135 1149
1136 1150 msg = _(u'Pull request updated to "{source_commit_id}" with '
1137 1151 u'{count_added} added, {count_removed} removed commits. '
1138 1152 u'Source of changes: {change_source}')
1139 1153 msg = msg.format(
1140 1154 source_commit_id=pull_request.source_ref_parts.commit_id,
1141 1155 count_added=len(resp.changes.added),
1142 1156 count_removed=len(resp.changes.removed),
1143 1157 change_source=changed)
1144 1158 h.flash(msg, category='success')
1145 1159
1146 1160 channel = '/repo${}$/pr/{}'.format(
1147 1161 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1148 1162 message = msg + (
1149 1163 ' - <a onclick="window.location.reload()">'
1150 1164 '<strong>{}</strong></a>'.format(_('Reload page')))
1151 1165 channelstream.post_message(
1152 1166 channel, message, self._rhodecode_user.username,
1153 1167 registry=self.request.registry)
1154 1168 else:
1155 1169 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1156 1170 warning_reasons = [
1157 1171 UpdateFailureReason.NO_CHANGE,
1158 1172 UpdateFailureReason.WRONG_REF_TYPE,
1159 1173 ]
1160 1174 category = 'warning' if resp.reason in warning_reasons else 'error'
1161 1175 h.flash(msg, category=category)
1162 1176
1163 1177 @LoginRequired()
1164 1178 @NotAnonymous()
1165 1179 @HasRepoPermissionAnyDecorator(
1166 1180 'repository.read', 'repository.write', 'repository.admin')
1167 1181 @CSRFRequired()
1168 1182 @view_config(
1169 1183 route_name='pullrequest_merge', request_method='POST',
1170 1184 renderer='json_ext')
1171 1185 def pull_request_merge(self):
1172 1186 """
1173 1187 Merge will perform a server-side merge of the specified
1174 1188 pull request, if the pull request is approved and mergeable.
1175 1189 After successful merging, the pull request is automatically
1176 1190 closed, with a relevant comment.
1177 1191 """
1178 1192 pull_request = PullRequest.get_or_404(
1179 1193 self.request.matchdict['pull_request_id'])
1180 1194 _ = self.request.translate
1181 1195
1182 1196 if pull_request.is_state_changing():
1183 1197 log.debug('show: forbidden because pull request is in state %s',
1184 1198 pull_request.pull_request_state)
1185 1199 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1186 1200 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1187 1201 pull_request.pull_request_state)
1188 1202 h.flash(msg, category='error')
1189 1203 raise HTTPFound(
1190 1204 h.route_path('pullrequest_show',
1191 1205 repo_name=pull_request.target_repo.repo_name,
1192 1206 pull_request_id=pull_request.pull_request_id))
1193 1207
1194 1208 self.load_default_context()
1195 1209
1196 1210 with pull_request.set_state(PullRequest.STATE_UPDATING):
1197 1211 check = MergeCheck.validate(
1198 1212 pull_request, auth_user=self._rhodecode_user,
1199 1213 translator=self.request.translate)
1200 1214 merge_possible = not check.failed
1201 1215
1202 1216 for err_type, error_msg in check.errors:
1203 1217 h.flash(error_msg, category=err_type)
1204 1218
1205 1219 if merge_possible:
1206 1220 log.debug("Pre-conditions checked, trying to merge.")
1207 1221 extras = vcs_operation_context(
1208 1222 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1209 1223 username=self._rhodecode_db_user.username, action='push',
1210 1224 scm=pull_request.target_repo.repo_type)
1211 1225 with pull_request.set_state(PullRequest.STATE_UPDATING):
1212 1226 self._merge_pull_request(
1213 1227 pull_request, self._rhodecode_db_user, extras)
1214 1228 else:
1215 1229 log.debug("Pre-conditions failed, NOT merging.")
1216 1230
1217 1231 raise HTTPFound(
1218 1232 h.route_path('pullrequest_show',
1219 1233 repo_name=pull_request.target_repo.repo_name,
1220 1234 pull_request_id=pull_request.pull_request_id))
1221 1235
1222 1236 def _merge_pull_request(self, pull_request, user, extras):
1223 1237 _ = self.request.translate
1224 1238 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1225 1239
1226 1240 if merge_resp.executed:
1227 1241 log.debug("The merge was successful, closing the pull request.")
1228 1242 PullRequestModel().close_pull_request(
1229 1243 pull_request.pull_request_id, user)
1230 1244 Session().commit()
1231 1245 msg = _('Pull request was successfully merged and closed.')
1232 1246 h.flash(msg, category='success')
1233 1247 else:
1234 1248 log.debug(
1235 1249 "The merge was not successful. Merge response: %s", merge_resp)
1236 1250 msg = merge_resp.merge_status_message
1237 1251 h.flash(msg, category='error')
1238 1252
1239 1253 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1240 1254 _ = self.request.translate
1241 1255
1242 1256 get_default_reviewers_data, validate_default_reviewers = \
1243 1257 PullRequestModel().get_reviewer_functions()
1244 1258
1245 1259 try:
1246 1260 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1247 1261 except ValueError as e:
1248 1262 log.error('Reviewers Validation: {}'.format(e))
1249 1263 h.flash(e, category='error')
1250 1264 return
1251 1265
1252 1266 old_calculated_status = pull_request.calculated_review_status()
1253 1267 PullRequestModel().update_reviewers(
1254 1268 pull_request, reviewers, self._rhodecode_user)
1255 1269 h.flash(_('Pull request reviewers updated.'), category='success')
1256 1270 Session().commit()
1257 1271
1258 1272 # trigger status changed if change in reviewers changes the status
1259 1273 calculated_status = pull_request.calculated_review_status()
1260 1274 if old_calculated_status != calculated_status:
1261 1275 PullRequestModel().trigger_pull_request_hook(
1262 1276 pull_request, self._rhodecode_user, 'review_status_change',
1263 1277 data={'status': calculated_status})
1264 1278
1265 1279 @LoginRequired()
1266 1280 @NotAnonymous()
1267 1281 @HasRepoPermissionAnyDecorator(
1268 1282 'repository.read', 'repository.write', 'repository.admin')
1269 1283 @CSRFRequired()
1270 1284 @view_config(
1271 1285 route_name='pullrequest_delete', request_method='POST',
1272 1286 renderer='json_ext')
1273 1287 def pull_request_delete(self):
1274 1288 _ = self.request.translate
1275 1289
1276 1290 pull_request = PullRequest.get_or_404(
1277 1291 self.request.matchdict['pull_request_id'])
1278 1292 self.load_default_context()
1279 1293
1280 1294 pr_closed = pull_request.is_closed()
1281 1295 allowed_to_delete = PullRequestModel().check_user_delete(
1282 1296 pull_request, self._rhodecode_user) and not pr_closed
1283 1297
1284 1298 # only owner can delete it !
1285 1299 if allowed_to_delete:
1286 1300 PullRequestModel().delete(pull_request, self._rhodecode_user)
1287 1301 Session().commit()
1288 1302 h.flash(_('Successfully deleted pull request'),
1289 1303 category='success')
1290 1304 raise HTTPFound(h.route_path('pullrequest_show_all',
1291 1305 repo_name=self.db_repo_name))
1292 1306
1293 1307 log.warning('user %s tried to delete pull request without access',
1294 1308 self._rhodecode_user)
1295 1309 raise HTTPNotFound()
1296 1310
1297 1311 @LoginRequired()
1298 1312 @NotAnonymous()
1299 1313 @HasRepoPermissionAnyDecorator(
1300 1314 'repository.read', 'repository.write', 'repository.admin')
1301 1315 @CSRFRequired()
1302 1316 @view_config(
1303 1317 route_name='pullrequest_comment_create', request_method='POST',
1304 1318 renderer='json_ext')
1305 1319 def pull_request_comment_create(self):
1306 1320 _ = self.request.translate
1307 1321
1308 1322 pull_request = PullRequest.get_or_404(
1309 1323 self.request.matchdict['pull_request_id'])
1310 1324 pull_request_id = pull_request.pull_request_id
1311 1325
1312 1326 if pull_request.is_closed():
1313 1327 log.debug('comment: forbidden because pull request is closed')
1314 1328 raise HTTPForbidden()
1315 1329
1316 1330 allowed_to_comment = PullRequestModel().check_user_comment(
1317 1331 pull_request, self._rhodecode_user)
1318 1332 if not allowed_to_comment:
1319 1333 log.debug(
1320 1334 'comment: forbidden because pull request is from forbidden repo')
1321 1335 raise HTTPForbidden()
1322 1336
1323 1337 c = self.load_default_context()
1324 1338
1325 1339 status = self.request.POST.get('changeset_status', None)
1326 1340 text = self.request.POST.get('text')
1327 1341 comment_type = self.request.POST.get('comment_type')
1328 1342 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1329 1343 close_pull_request = self.request.POST.get('close_pull_request')
1330 1344
1331 1345 # the logic here should work like following, if we submit close
1332 1346 # pr comment, use `close_pull_request_with_comment` function
1333 1347 # else handle regular comment logic
1334 1348
1335 1349 if close_pull_request:
1336 1350 # only owner or admin or person with write permissions
1337 1351 allowed_to_close = PullRequestModel().check_user_update(
1338 1352 pull_request, self._rhodecode_user)
1339 1353 if not allowed_to_close:
1340 1354 log.debug('comment: forbidden because not allowed to close '
1341 1355 'pull request %s', pull_request_id)
1342 1356 raise HTTPForbidden()
1343 1357
1344 1358 # This also triggers `review_status_change`
1345 1359 comment, status = PullRequestModel().close_pull_request_with_comment(
1346 1360 pull_request, self._rhodecode_user, self.db_repo, message=text,
1347 1361 auth_user=self._rhodecode_user)
1348 1362 Session().flush()
1349 1363
1350 1364 PullRequestModel().trigger_pull_request_hook(
1351 1365 pull_request, self._rhodecode_user, 'comment',
1352 1366 data={'comment': comment})
1353 1367
1354 1368 else:
1355 1369 # regular comment case, could be inline, or one with status.
1356 1370 # for that one we check also permissions
1357 1371
1358 1372 allowed_to_change_status = PullRequestModel().check_user_change_status(
1359 1373 pull_request, self._rhodecode_user)
1360 1374
1361 1375 if status and allowed_to_change_status:
1362 1376 message = (_('Status change %(transition_icon)s %(status)s')
1363 1377 % {'transition_icon': '>',
1364 1378 'status': ChangesetStatus.get_status_lbl(status)})
1365 1379 text = text or message
1366 1380
1367 1381 comment = CommentsModel().create(
1368 1382 text=text,
1369 1383 repo=self.db_repo.repo_id,
1370 1384 user=self._rhodecode_user.user_id,
1371 1385 pull_request=pull_request,
1372 1386 f_path=self.request.POST.get('f_path'),
1373 1387 line_no=self.request.POST.get('line'),
1374 1388 status_change=(ChangesetStatus.get_status_lbl(status)
1375 1389 if status and allowed_to_change_status else None),
1376 1390 status_change_type=(status
1377 1391 if status and allowed_to_change_status else None),
1378 1392 comment_type=comment_type,
1379 1393 resolves_comment_id=resolves_comment_id,
1380 1394 auth_user=self._rhodecode_user
1381 1395 )
1382 1396
1383 1397 if allowed_to_change_status:
1384 1398 # calculate old status before we change it
1385 1399 old_calculated_status = pull_request.calculated_review_status()
1386 1400
1387 1401 # get status if set !
1388 1402 if status:
1389 1403 ChangesetStatusModel().set_status(
1390 1404 self.db_repo.repo_id,
1391 1405 status,
1392 1406 self._rhodecode_user.user_id,
1393 1407 comment,
1394 1408 pull_request=pull_request
1395 1409 )
1396 1410
1397 1411 Session().flush()
1398 1412 # this is somehow required to get access to some relationship
1399 1413 # loaded on comment
1400 1414 Session().refresh(comment)
1401 1415
1402 1416 PullRequestModel().trigger_pull_request_hook(
1403 1417 pull_request, self._rhodecode_user, 'comment',
1404 1418 data={'comment': comment})
1405 1419
1406 1420 # we now calculate the status of pull request, and based on that
1407 1421 # calculation we set the commits status
1408 1422 calculated_status = pull_request.calculated_review_status()
1409 1423 if old_calculated_status != calculated_status:
1410 1424 PullRequestModel().trigger_pull_request_hook(
1411 1425 pull_request, self._rhodecode_user, 'review_status_change',
1412 1426 data={'status': calculated_status})
1413 1427
1414 1428 Session().commit()
1415 1429
1416 1430 data = {
1417 1431 'target_id': h.safeid(h.safe_unicode(
1418 1432 self.request.POST.get('f_path'))),
1419 1433 }
1420 1434 if comment:
1421 1435 c.co = comment
1422 1436 rendered_comment = render(
1423 1437 'rhodecode:templates/changeset/changeset_comment_block.mako',
1424 1438 self._get_template_context(c), self.request)
1425 1439
1426 1440 data.update(comment.get_dict())
1427 1441 data.update({'rendered_text': rendered_comment})
1428 1442
1429 1443 return data
1430 1444
1431 1445 @LoginRequired()
1432 1446 @NotAnonymous()
1433 1447 @HasRepoPermissionAnyDecorator(
1434 1448 'repository.read', 'repository.write', 'repository.admin')
1435 1449 @CSRFRequired()
1436 1450 @view_config(
1437 1451 route_name='pullrequest_comment_delete', request_method='POST',
1438 1452 renderer='json_ext')
1439 1453 def pull_request_comment_delete(self):
1440 1454 pull_request = PullRequest.get_or_404(
1441 1455 self.request.matchdict['pull_request_id'])
1442 1456
1443 1457 comment = ChangesetComment.get_or_404(
1444 1458 self.request.matchdict['comment_id'])
1445 1459 comment_id = comment.comment_id
1446 1460
1447 1461 if pull_request.is_closed():
1448 1462 log.debug('comment: forbidden because pull request is closed')
1449 1463 raise HTTPForbidden()
1450 1464
1451 1465 if not comment:
1452 1466 log.debug('Comment with id:%s not found, skipping', comment_id)
1453 1467 # comment already deleted in another call probably
1454 1468 return True
1455 1469
1456 1470 if comment.pull_request.is_closed():
1457 1471 # don't allow deleting comments on closed pull request
1458 1472 raise HTTPForbidden()
1459 1473
1460 1474 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1461 1475 super_admin = h.HasPermissionAny('hg.admin')()
1462 1476 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1463 1477 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1464 1478 comment_repo_admin = is_repo_admin and is_repo_comment
1465 1479
1466 1480 if super_admin or comment_owner or comment_repo_admin:
1467 1481 old_calculated_status = comment.pull_request.calculated_review_status()
1468 1482 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1469 1483 Session().commit()
1470 1484 calculated_status = comment.pull_request.calculated_review_status()
1471 1485 if old_calculated_status != calculated_status:
1472 1486 PullRequestModel().trigger_pull_request_hook(
1473 1487 comment.pull_request, self._rhodecode_user, 'review_status_change',
1474 1488 data={'status': calculated_status})
1475 1489 return True
1476 1490 else:
1477 1491 log.warning('No permissions for user %s to delete comment_id: %s',
1478 1492 self._rhodecode_db_user, comment_id)
1479 1493 raise HTTPNotFound()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,907 +1,912 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="breadcrumbs_links()">
13 13
14 14 <div id="pr-title">
15 15 % if c.pull_request.is_closed():
16 16 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
17 17 % endif
18 18 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
19 19 </div>
20 20 <div id="pr-title-edit" class="input" style="display: none;">
21 21 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
22 22 </div>
23 23 </%def>
24 24
25 25 <%def name="menu_bar_nav()">
26 26 ${self.menu_items(active='repositories')}
27 27 </%def>
28 28
29 29 <%def name="menu_bar_subnav()">
30 30 ${self.repo_menu(active='showpullrequest')}
31 31 </%def>
32 32
33 33 <%def name="main()">
34 34
35 35 <script type="text/javascript">
36 36 // TODO: marcink switch this to pyroutes
37 37 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__')}";
38 38 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
39 39 </script>
40 40
41 41 <div class="box">
42 42
43 43 ${self.breadcrumbs()}
44 44
45 45 <div class="box pr-summary">
46 46
47 47 <div class="summary-details block-left">
48 48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 49 <div class="pr-details-title">
50 50 <div class="pull-left">
51 51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
52 52 ${_('Created on')}
53 53 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
54 54 <span class="pr-details-title-author-pref">${_('by')}</span>
55 55 </div>
56 56
57 57 <div class="pull-left">
58 58 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
59 59 </div>
60 60
61 61 %if c.allowed_to_update:
62 62 <div class="pull-right">
63 63 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
64 64 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
65 65 % if c.allowed_to_delete:
66 66 ${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)}
67 67 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete pull request'),
68 68 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
69 69 ${h.end_form()}
70 70 % else:
71 71 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
72 72 % endif
73 73 </div>
74 74 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
75 75 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
76 76 </div>
77 77
78 78 %endif
79 79 </div>
80 80
81 81 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
82 82 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
83 83 </div>
84 84
85 85 <div id="pr-desc-edit" class="input textarea" style="display: none;">
86 86 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
87 87 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
88 88 </div>
89 89
90 90 <div id="summary" class="fields pr-details-content">
91 91
92 92 ## review
93 93 <div class="field">
94 94 <div class="label-pr-detail">
95 95 <label>${_('Review status')}:</label>
96 96 </div>
97 97 <div class="input">
98 98 %if c.pull_request_review_status:
99 99 <div class="tag status-tag-${c.pull_request_review_status}">
100 100 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
101 101 <span class="changeset-status-lbl">
102 102 %if c.pull_request.is_closed():
103 103 ${_('Closed')},
104 104 %endif
105 105
106 106 ${h.commit_status_lbl(c.pull_request_review_status)}
107 107
108 108 </span>
109 109 </div>
110 110 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
111 111 %endif
112 112 </div>
113 113 </div>
114 114
115 115 ## source
116 116 <div class="field">
117 117 <div class="label-pr-detail">
118 118 <label>${_('Commit flow')}:</label>
119 119 </div>
120 120 <div class="input">
121 121 <div class="pr-commit-flow">
122 122 ## Source
123 123 %if c.pull_request.source_ref_parts.type == 'branch':
124 124 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
125 125 %else:
126 126 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
127 127 %endif
128 128 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
129 129 &rarr;
130 130 ## Target
131 131 %if c.pull_request.target_ref_parts.type == 'branch':
132 132 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
133 133 %else:
134 134 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
135 135 %endif
136 136
137 137 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
138 138
139 139 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
140 140 <i class="icon-angle-down">more details</i>
141 141 </a>
142 142
143 143 </div>
144 144
145 145 <div class="source-details" style="display: none">
146 146
147 147 <ul>
148 148
149 149 ## common ancestor
150 150 <li>
151 151 ${_('Common ancestor')}:
152 152 % if c.ancestor_commit:
153 153 <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>
154 154 % else:
155 155 ${_('not available')}
156 156 % endif
157 157 </li>
158 158
159 159 ## pull url
160 160 <li>
161 161 %if h.is_hg(c.pull_request.source_repo):
162 162 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
163 163 %elif h.is_git(c.pull_request.source_repo):
164 164 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
165 165 %endif
166 166
167 167 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
168 168 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
169 169 </li>
170 170
171 171 ## Shadow repo
172 172 <li>
173 173 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
174 174 %if h.is_hg(c.pull_request.target_repo):
175 175 <% 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) %>
176 176 %elif h.is_git(c.pull_request.target_repo):
177 177 <% 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) %>
178 178 %endif
179 179
180 180 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
181 181 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
182 182
183 183 % else:
184 184 <div class="">
185 185 ${_('Shadow repository data not available')}.
186 186 </div>
187 187 % endif
188 188 </li>
189 189
190 190 </ul>
191 191
192 192 </div>
193 193
194 194 </div>
195 195
196 196 </div>
197 197
198 198 ## versions
199 199 <div class="field">
200 200 <div class="label-pr-detail">
201 201 <label>${_('Versions')}:</label>
202 202 </div>
203 203
204 204 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
205 205 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
206 206
207 207 <div class="pr-versions">
208 208 % if c.show_version_changes:
209 209 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
210 210 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
211 211 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
212 212 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
213 213 data-toggle-on="${_('show versions')}."
214 214 data-toggle-off="${_('hide versions')}.">
215 215 ${_('show versions')}.
216 216 </a>
217 217 <table>
218 218 ## SHOW ALL VERSIONS OF PR
219 219 <% ver_pr = None %>
220 220
221 221 % for data in reversed(list(enumerate(c.versions, 1))):
222 222 <% ver_pos = data[0] %>
223 223 <% ver = data[1] %>
224 224 <% ver_pr = ver.pull_request_version_id %>
225 225 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
226 226
227 227 <tr class="version-pr" style="display: ${display_row}">
228 228 <td>
229 229 <code>
230 230 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
231 231 </code>
232 232 </td>
233 233 <td>
234 234 <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}"/>
235 235 <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}"/>
236 236 </td>
237 237 <td>
238 238 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
239 239 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
240 240
241 241 </td>
242 242 <td>
243 243 % if c.at_version_num != ver_pr:
244 244 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
245 245 <code>
246 246 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
247 247 </code>
248 248 % endif
249 249 </td>
250 250 <td>
251 251 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
252 252 </td>
253 253 <td>
254 254 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
255 255 </td>
256 256 </tr>
257 257 % endfor
258 258
259 259 <tr>
260 260 <td colspan="6">
261 261 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
262 262 data-label-text-locked="${_('select versions to show changes')}"
263 263 data-label-text-diff="${_('show changes between versions')}"
264 264 data-label-text-show="${_('show pull request for this version')}"
265 265 >
266 266 ${_('select versions to show changes')}
267 267 </button>
268 268 </td>
269 269 </tr>
270 270 </table>
271 271 % else:
272 272 <div>
273 273 ${_('Pull request versions not available')}.
274 274 </div>
275 275 % endif
276 276 </div>
277 277 </div>
278 278
279 279 </div>
280 280
281 281 </div>
282 282
283 283 ## REVIEW RULES
284 284 <div id="review_rules" style="display: none" class="reviewers-title block-right">
285 285 <div class="pr-details-title">
286 286 ${_('Reviewer rules')}
287 287 %if c.allowed_to_update:
288 288 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
289 289 %endif
290 290 </div>
291 291 <div class="pr-reviewer-rules">
292 292 ## review rules will be appended here, by default reviewers logic
293 293 </div>
294 294 <input id="review_data" type="hidden" name="review_data" value="">
295 295 </div>
296 296
297 297 ## REVIEWERS
298 298 <div class="reviewers-title block-right">
299 299 <div class="pr-details-title">
300 300 ${_('Pull request reviewers')}
301 301 %if c.allowed_to_update:
302 302 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
303 303 %endif
304 304 </div>
305 305 </div>
306 306 <div id="reviewers" class="block-right pr-details-content reviewers">
307 307
308 308 ## members redering block
309 309 <input type="hidden" name="__start__" value="review_members:sequence">
310 310 <ul id="review_members" class="group_members">
311 311
312 312 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
313 313 <script>
314 314 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
315 315 var status = "${(status[0][1].status if status else 'not_reviewed')}";
316 316 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
317 317 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
318 318
319 319 var entry = renderTemplate('reviewMemberEntry', {
320 320 'member': member,
321 321 'mandatory': member.mandatory,
322 322 'reasons': member.reasons,
323 323 'allowed_to_update': allowed_to_update,
324 324 'review_status': status,
325 325 'review_status_label': status_lbl,
326 326 'user_group': member.user_group,
327 327 'create': false
328 328 });
329 329 $('#review_members').append(entry)
330 330 </script>
331 331
332 332 % endfor
333 333
334 334 </ul>
335 335
336 336 <input type="hidden" name="__end__" value="review_members:sequence">
337 337 ## end members redering block
338 338
339 339 %if not c.pull_request.is_closed():
340 340 <div id="add_reviewer" class="ac" style="display: none;">
341 341 %if c.allowed_to_update:
342 342 % if not c.forbid_adding_reviewers:
343 343 <div id="add_reviewer_input" class="reviewer_ac">
344 344 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
345 345 <div id="reviewers_container"></div>
346 346 </div>
347 347 % endif
348 348 <div class="pull-right">
349 349 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
350 350 </div>
351 351 %endif
352 352 </div>
353 353 %endif
354 354 </div>
355 355
356 356 ## TODOs will be listed here
357 357 <div class="reviewers-title block-right">
358 358 <div class="pr-details-title">
359 359 ## Only show unresolved, that is only what matters
360 360 TODO Comments - ${len(c.unresolved_comments)} / ${(len(c.unresolved_comments) + len(c.resolved_comments))}
361 361
362 362 % if not c.at_version:
363 363 % if c.resolved_comments:
364 364 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
365 365 % else:
366 366 <span class="block-right last-item noselect">Show resolved</span>
367 367 % endif
368 368 % endif
369 369 </div>
370 370 </div>
371 371 <div class="block-right pr-details-content reviewers">
372 372
373 373 <table class="todo-table">
374 374 <%
375 375 def sorter(entry):
376 376 user_id = entry.author.user_id
377 377 resolved = '1' if entry.resolved else '0'
378 378 if user_id == c.rhodecode_user.user_id:
379 379 # own comments first
380 380 user_id = 0
381 381 return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
382 382 %>
383 383
384 384 % if c.at_version:
385 385 <tr>
386 386 <td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
387 387 </tr>
388 388 % else:
389 389 % for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
390 390 <% resolved = todo_comment.resolved %>
391 391 % if inline:
392 392 <% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
393 393 % else:
394 394 <% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
395 395 % endif
396 396
397 397 <tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
398 398
399 399 <td class="td-todo-number">
400 400 % if resolved:
401 401 <a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
402 402 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
403 403 % else:
404 404 <a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
405 405 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
406 406 % endif
407 407 </td>
408 408 <td class="td-todo-gravatar">
409 409 ${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
410 410 </td>
411 411 <td class="todo-comment-text-wrapper">
412 412 <div class="todo-comment-text">
413 413 <code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
414 414 </div>
415 415 </td>
416 416
417 417 </tr>
418 418 % endfor
419 419
420 420 % if len(c.unresolved_comments) == 0:
421 421 <tr>
422 422 <td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
423 423 </tr>
424 424 % endif
425 425
426 426 % endif
427 427
428 428 </table>
429 429
430 430 </div>
431 431 </div>
432 432
433 433 </div>
434 434
435 435 <div class="box">
436 436
437 437 % if c.state_progressing:
438 438
439 439 <h2 style="text-align: center">
440 440 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
441
442 % if c.is_super_admin:
443 <br/>
444 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
445 % endif
441 446 </h2>
442 447
443 448 % else:
444 449
445 450 ## Diffs rendered here
446 451 <div class="table" >
447 452 <div id="changeset_compare_view_content">
448 453 ##CS
449 454 % if c.missing_requirements:
450 455 <div class="box">
451 456 <div class="alert alert-warning">
452 457 <div>
453 458 <strong>${_('Missing requirements:')}</strong>
454 459 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
455 460 </div>
456 461 </div>
457 462 </div>
458 463 % elif c.missing_commits:
459 464 <div class="box">
460 465 <div class="alert alert-warning">
461 466 <div>
462 467 <strong>${_('Missing commits')}:</strong>
463 468 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
464 469 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
465 470 ${_('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}
466 471 </div>
467 472 </div>
468 473 </div>
469 474 % endif
470 475
471 476 <div class="compare_view_commits_title">
472 477 % if not c.compare_mode:
473 478
474 479 % if c.at_version_pos:
475 480 <h4>
476 481 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
477 482 </h4>
478 483 % endif
479 484
480 485 <div class="pull-left">
481 486 <div class="btn-group">
482 487 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
483 488 % if c.collapse_all_commits:
484 489 <i class="icon-plus-squared-alt icon-no-margin"></i>
485 490 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
486 491 % else:
487 492 <i class="icon-minus-squared-alt icon-no-margin"></i>
488 493 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
489 494 % endif
490 495 </a>
491 496 </div>
492 497 </div>
493 498
494 499 <div class="pull-right">
495 500 % if c.allowed_to_update and not c.pull_request.is_closed():
496 501
497 502 <div class="btn-group btn-group-actions">
498 503 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
499 504 ${_('Update commits')}
500 505 </a>
501 506
502 507 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
503 508 <i class="icon-down"></i>
504 509 </a>
505 510
506 511 <div class="btn-action-switcher-container" id="update-commits-switcher">
507 512 <ul class="btn-action-switcher" role="menu">
508 513 <li>
509 514 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
510 515 ${_('Force update commits')}
511 516 </a>
512 517 <div class="action-help-block">
513 518 ${_('Update commits and force refresh this pull request.')}
514 519 </div>
515 520 </li>
516 521 </ul>
517 522 </div>
518 523 </div>
519 524
520 525 % else:
521 526 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
522 527 % endif
523 528
524 529 </div>
525 530 % endif
526 531 </div>
527 532
528 533 % if not c.missing_commits:
529 534 % if c.compare_mode:
530 535 % if c.at_version:
531 536 <h4>
532 537 ${_('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')}:
533 538 </h4>
534 539
535 540 <div class="subtitle-compare">
536 541 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
537 542 </div>
538 543
539 544 <div class="container">
540 545 <table class="rctable compare_view_commits">
541 546 <tr>
542 547 <th></th>
543 548 <th>${_('Time')}</th>
544 549 <th>${_('Author')}</th>
545 550 <th>${_('Commit')}</th>
546 551 <th></th>
547 552 <th>${_('Description')}</th>
548 553 </tr>
549 554
550 555 % for c_type, commit in c.commit_changes:
551 556 % if c_type in ['a', 'r']:
552 557 <%
553 558 if c_type == 'a':
554 559 cc_title = _('Commit added in displayed changes')
555 560 elif c_type == 'r':
556 561 cc_title = _('Commit removed in displayed changes')
557 562 else:
558 563 cc_title = ''
559 564 %>
560 565 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
561 566 <td>
562 567 <div class="commit-change-indicator color-${c_type}-border">
563 568 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
564 569 ${c_type.upper()}
565 570 </div>
566 571 </div>
567 572 </td>
568 573 <td class="td-time">
569 574 ${h.age_component(commit.date)}
570 575 </td>
571 576 <td class="td-user">
572 577 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
573 578 </td>
574 579 <td class="td-hash">
575 580 <code>
576 581 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
577 582 r${commit.idx}:${h.short_id(commit.raw_id)}
578 583 </a>
579 584 ${h.hidden('revisions', commit.raw_id)}
580 585 </code>
581 586 </td>
582 587 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
583 588 <i class="icon-expand-linked"></i>
584 589 </td>
585 590 <td class="mid td-description">
586 591 <div class="log-container truncate-wrap">
587 592 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
588 593 </div>
589 594 </td>
590 595 </tr>
591 596 % endif
592 597 % endfor
593 598 </table>
594 599 </div>
595 600
596 601 % endif
597 602
598 603 % else:
599 604 <%include file="/compare/compare_commits.mako" />
600 605 % endif
601 606
602 607 <div class="cs_files">
603 608 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
604 609 % if c.at_version:
605 610 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
606 611 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
607 612 % else:
608 613 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
609 614 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
610 615 % endif
611 616
612 617 <%
613 618 pr_menu_data = {
614 619 'outdated_comm_count_ver': outdated_comm_count_ver
615 620 }
616 621 %>
617 622
618 623 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
619 624
620 625 % if c.range_diff_on:
621 626 % for commit in c.commit_ranges:
622 627 ${cbdiffs.render_diffset(
623 628 c.changes[commit.raw_id],
624 629 commit=commit, use_comments=True,
625 630 collapse_when_files_over=5,
626 631 disable_new_comments=True,
627 632 deleted_files_comments=c.deleted_files_comments,
628 633 inline_comments=c.inline_comments,
629 634 pull_request_menu=pr_menu_data, show_todos=False)}
630 635 % endfor
631 636 % else:
632 637 ${cbdiffs.render_diffset(
633 638 c.diffset, use_comments=True,
634 639 collapse_when_files_over=30,
635 640 disable_new_comments=not c.allowed_to_comment,
636 641 deleted_files_comments=c.deleted_files_comments,
637 642 inline_comments=c.inline_comments,
638 643 pull_request_menu=pr_menu_data, show_todos=False)}
639 644 % endif
640 645
641 646 </div>
642 647 % else:
643 648 ## skipping commits we need to clear the view for missing commits
644 649 <div style="clear:both;"></div>
645 650 % endif
646 651
647 652 </div>
648 653 </div>
649 654
650 655 ## template for inline comment form
651 656 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
652 657
653 658 ## comments heading with count
654 659 <div class="comments-heading">
655 660 <i class="icon-comment"></i>
656 661 ${_('Comments')} ${len(c.comments)}
657 662 </div>
658 663
659 664 ## render general comments
660 665 <div id="comment-tr-show">
661 666 % if general_outdated_comm_count_ver:
662 667 <div class="info-box">
663 668 % if general_outdated_comm_count_ver == 1:
664 669 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
665 670 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
666 671 % else:
667 672 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
668 673 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
669 674 % endif
670 675 </div>
671 676 % endif
672 677 </div>
673 678
674 679 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
675 680
676 681 % if not c.pull_request.is_closed():
677 682 ## main comment form and it status
678 683 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
679 684 pull_request_id=c.pull_request.pull_request_id),
680 685 c.pull_request_review_status,
681 686 is_pull_request=True, change_status=c.allowed_to_change_status)}
682 687
683 688 ## merge status, and merge action
684 689 <div class="pull-request-merge">
685 690 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
686 691 </div>
687 692
688 693 %endif
689 694
690 695 % endif
691 696 </div>
692 697
693 698 <script type="text/javascript">
694 699
695 700 versionController = new VersionController();
696 701 versionController.init();
697 702
698 703 reviewersController = new ReviewersController();
699 704 commitsController = new CommitsController();
700 705
701 706 updateController = new UpdatePrController();
702 707
703 708 $(function () {
704 709
705 710 // custom code mirror
706 711 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
707 712
708 713 var PRDetails = {
709 714 editButton: $('#open_edit_pullrequest'),
710 715 closeButton: $('#close_edit_pullrequest'),
711 716 deleteButton: $('#delete_pullrequest'),
712 717 viewFields: $('#pr-desc, #pr-title'),
713 718 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
714 719
715 720 init: function () {
716 721 var that = this;
717 722 this.editButton.on('click', function (e) {
718 723 that.edit();
719 724 });
720 725 this.closeButton.on('click', function (e) {
721 726 that.view();
722 727 });
723 728 },
724 729
725 730 edit: function (event) {
726 731 this.viewFields.hide();
727 732 this.editButton.hide();
728 733 this.deleteButton.hide();
729 734 this.closeButton.show();
730 735 this.editFields.show();
731 736 codeMirrorInstance.refresh();
732 737 },
733 738
734 739 view: function (event) {
735 740 this.editButton.show();
736 741 this.deleteButton.show();
737 742 this.editFields.hide();
738 743 this.closeButton.hide();
739 744 this.viewFields.show();
740 745 }
741 746 };
742 747
743 748 var ReviewersPanel = {
744 749 editButton: $('#open_edit_reviewers'),
745 750 closeButton: $('#close_edit_reviewers'),
746 751 addButton: $('#add_reviewer'),
747 752 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
748 753
749 754 init: function () {
750 755 var self = this;
751 756 this.editButton.on('click', function (e) {
752 757 self.edit();
753 758 });
754 759 this.closeButton.on('click', function (e) {
755 760 self.close();
756 761 });
757 762 },
758 763
759 764 edit: function (event) {
760 765 this.editButton.hide();
761 766 this.closeButton.show();
762 767 this.addButton.show();
763 768 this.removeButtons.css('visibility', 'visible');
764 769 // review rules
765 770 reviewersController.loadReviewRules(
766 771 ${c.pull_request.reviewer_data_json | n});
767 772 },
768 773
769 774 close: function (event) {
770 775 this.editButton.show();
771 776 this.closeButton.hide();
772 777 this.addButton.hide();
773 778 this.removeButtons.css('visibility', 'hidden');
774 779 // hide review rules
775 780 reviewersController.hideReviewRules()
776 781 }
777 782 };
778 783
779 784 PRDetails.init();
780 785 ReviewersPanel.init();
781 786
782 787 showOutdated = function (self) {
783 788 $('.comment-inline.comment-outdated').show();
784 789 $('.filediff-outdated').show();
785 790 $('.showOutdatedComments').hide();
786 791 $('.hideOutdatedComments').show();
787 792 };
788 793
789 794 hideOutdated = function (self) {
790 795 $('.comment-inline.comment-outdated').hide();
791 796 $('.filediff-outdated').hide();
792 797 $('.hideOutdatedComments').hide();
793 798 $('.showOutdatedComments').show();
794 799 };
795 800
796 801 refreshMergeChecks = function () {
797 802 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
798 803 $('.pull-request-merge').css('opacity', 0.3);
799 804 $('.action-buttons-extra').css('opacity', 0.3);
800 805
801 806 $('.pull-request-merge').load(
802 807 loadUrl, function () {
803 808 $('.pull-request-merge').css('opacity', 1);
804 809
805 810 $('.action-buttons-extra').css('opacity', 1);
806 811 }
807 812 );
808 813 };
809 814
810 815 closePullRequest = function (status) {
811 816 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
812 817 return false;
813 818 }
814 819 // inject closing flag
815 820 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
816 821 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
817 822 $(generalCommentForm.submitForm).submit();
818 823 };
819 824
820 825 $('#show-outdated-comments').on('click', function (e) {
821 826 var button = $(this);
822 827 var outdated = $('.comment-outdated');
823 828
824 829 if (button.html() === "(Show)") {
825 830 button.html("(Hide)");
826 831 outdated.show();
827 832 } else {
828 833 button.html("(Show)");
829 834 outdated.hide();
830 835 }
831 836 });
832 837
833 838 $('.show-inline-comments').on('change', function (e) {
834 839 var show = 'none';
835 840 var target = e.currentTarget;
836 841 if (target.checked) {
837 842 show = ''
838 843 }
839 844 var boxid = $(target).attr('id_for');
840 845 var comments = $('#{0} .inline-comments'.format(boxid));
841 846 var fn_display = function (idx) {
842 847 $(this).css('display', show);
843 848 };
844 849 $(comments).each(fn_display);
845 850 var btns = $('#{0} .inline-comments-button'.format(boxid));
846 851 $(btns).each(fn_display);
847 852 });
848 853
849 854 $('#merge_pull_request_form').submit(function () {
850 855 if (!$('#merge_pull_request').attr('disabled')) {
851 856 $('#merge_pull_request').attr('disabled', 'disabled');
852 857 }
853 858 return true;
854 859 });
855 860
856 861 $('#edit_pull_request').on('click', function (e) {
857 862 var title = $('#pr-title-input').val();
858 863 var description = codeMirrorInstance.getValue();
859 864 var renderer = $('#pr-renderer-input').val();
860 865 editPullRequest(
861 866 "${c.repo_name}", "${c.pull_request.pull_request_id}",
862 867 title, description, renderer);
863 868 });
864 869
865 870 $('#update_pull_request').on('click', function (e) {
866 871 $(this).attr('disabled', 'disabled');
867 872 $(this).addClass('disabled');
868 873 $(this).html(_gettext('Saving...'));
869 874 reviewersController.updateReviewers(
870 875 "${c.repo_name}", "${c.pull_request.pull_request_id}");
871 876 });
872 877
873 878
874 879 // fixing issue with caches on firefox
875 880 $('#update_commits').removeAttr("disabled");
876 881
877 882 $('.show-inline-comments').on('click', function (e) {
878 883 var boxid = $(this).attr('data-comment-id');
879 884 var button = $(this);
880 885
881 886 if (button.hasClass("comments-visible")) {
882 887 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
883 888 $(this).hide();
884 889 });
885 890 button.removeClass("comments-visible");
886 891 } else {
887 892 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
888 893 $(this).show();
889 894 });
890 895 button.addClass("comments-visible");
891 896 }
892 897 });
893 898
894 899 // register submit callback on commentForm form to track TODOs
895 900 window.commentFormGlobalSubmitSuccessCallback = function () {
896 901 refreshMergeChecks();
897 902 };
898 903
899 904 ReviewerAutoComplete('#user');
900 905
901 906 })
902 907
903 908 </script>
904 909
905 910 </div>
906 911
907 912 </%def>
General Comments 0
You need to be logged in to leave comments. Login now