##// END OF EJS Templates
pull-requests: allow forced state change to repo admins too.
super-admin -
r4710:97a223d9 stable
parent child Browse files
Show More
@@ -1,1872 +1,1873 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name,
83 83 search_q=search_q, statuses=statuses,
84 84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name,
87 87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, self._rhodecode_user.user_id,
91 91 search_q=search_q, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, self._rhodecode_user.user_id,
95 95 search_q=search_q, statuses=statuses)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments_count = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr,
110 110 include_drafts=False, count_only=True)
111 111
112 112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 114 if review_statuses and review_statuses[4]:
115 115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 116 my_review_status = statuses[0][1].status
117 117
118 118 data.append({
119 119 'name': _render('pullrequest_name',
120 120 pr.pull_request_id, pr.pull_request_state,
121 121 pr.work_in_progress, pr.target_repo.repo_name,
122 122 short=True),
123 123 'name_raw': pr.pull_request_id,
124 124 'status': _render('pullrequest_status',
125 125 pr.calculated_review_status()),
126 126 'my_status': _render('pullrequest_status',
127 127 my_review_status),
128 128 'title': _render('pullrequest_title', pr.title, pr.description),
129 129 'description': h.escape(pr.description),
130 130 'updated_on': _render('pullrequest_updated_on',
131 131 h.datetime_to_time(pr.updated_on),
132 132 pr.versions_count),
133 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 134 'created_on': _render('pullrequest_updated_on',
135 135 h.datetime_to_time(pr.created_on)),
136 136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 137 'state': pr.pull_request_state,
138 138 'author': _render('pullrequest_author',
139 139 pr.author.full_contact, ),
140 140 'author_raw': pr.author.full_name,
141 141 'comments': _render('pullrequest_comments', comments_count),
142 142 'comments_raw': comments_count,
143 143 'closed': pr.is_closed(),
144 144 })
145 145
146 146 data = ({
147 147 'draw': draw,
148 148 'data': data,
149 149 'recordsTotal': pull_requests_total_count,
150 150 'recordsFiltered': pull_requests_total_count,
151 151 })
152 152 return data
153 153
154 154 @LoginRequired()
155 155 @HasRepoPermissionAnyDecorator(
156 156 'repository.read', 'repository.write', 'repository.admin')
157 157 def pull_request_list(self):
158 158 c = self.load_default_context()
159 159
160 160 req_get = self.request.GET
161 161 c.source = str2bool(req_get.get('source'))
162 162 c.closed = str2bool(req_get.get('closed'))
163 163 c.my = str2bool(req_get.get('my'))
164 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 166
167 167 c.active = 'open'
168 168 if c.my:
169 169 c.active = 'my'
170 170 if c.closed:
171 171 c.active = 'closed'
172 172 if c.awaiting_review and not c.source:
173 173 c.active = 'awaiting'
174 174 if c.source and not c.awaiting_review:
175 175 c.active = 'source'
176 176 if c.awaiting_my_review:
177 177 c.active = 'awaiting_my'
178 178
179 179 return self._get_template_context(c)
180 180
181 181 @LoginRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 ancestor_commit,
221 221 source_ref_id, target_ref_id,
222 222 target_commit, source_commit, diff_limit, file_limit,
223 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 224
225 225 target_commit_final = target_commit
226 226 source_commit_final = source_commit
227 227
228 228 if use_ancestor:
229 229 # we might want to not use it for versions
230 230 target_ref_id = ancestor_commit.raw_id
231 231 target_commit_final = ancestor_commit
232 232
233 233 vcs_diff = PullRequestModel().get_diff(
234 234 source_repo, source_ref_id, target_ref_id,
235 235 hide_whitespace_changes, diff_context)
236 236
237 237 diff_processor = diffs.DiffProcessor(
238 238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 239 file_limit=file_limit, show_full_diff=fulldiff)
240 240
241 241 _parsed = diff_processor.prepare()
242 242
243 243 diffset = codeblocks.DiffSet(
244 244 repo_name=self.db_repo_name,
245 245 source_repo_name=source_repo_name,
246 246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 248 )
249 249 diffset = self.path_filter.render_patchset_filtered(
250 250 diffset, _parsed, target_ref_id, source_ref_id)
251 251
252 252 return diffset
253 253
254 254 def _get_range_diffset(self, source_scm, source_repo,
255 255 commit1, commit2, diff_limit, file_limit,
256 256 fulldiff, hide_whitespace_changes, diff_context):
257 257 vcs_diff = source_scm.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(
263 263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 264 file_limit=file_limit, show_full_diff=fulldiff)
265 265
266 266 _parsed = diff_processor.prepare()
267 267
268 268 diffset = codeblocks.DiffSet(
269 269 repo_name=source_repo.repo_name,
270 270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 272
273 273 diffset = self.path_filter.render_patchset_filtered(
274 274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 275
276 276 return diffset
277 277
278 278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 279 comments_model = CommentsModel()
280 280
281 281 # GENERAL COMMENTS with versions #
282 282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 283 q = q.order_by(ChangesetComment.comment_id.asc())
284 284 if not include_drafts:
285 285 q = q.filter(ChangesetComment.draft == false())
286 286 general_comments = q
287 287
288 288 # pick comments we want to render at current version
289 289 c.comment_versions = comments_model.aggregate_comments(
290 290 general_comments, versions, c.at_version_num)
291 291
292 292 # INLINE COMMENTS with versions #
293 293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 294 q = q.order_by(ChangesetComment.comment_id.asc())
295 295 if not include_drafts:
296 296 q = q.filter(ChangesetComment.draft == false())
297 297 inline_comments = q
298 298
299 299 c.inline_versions = comments_model.aggregate_comments(
300 300 inline_comments, versions, c.at_version_num, inline=True)
301 301
302 302 # Comments inline+general
303 303 if c.at_version:
304 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 305 c.comments = c.comment_versions[c.at_version_num]['display']
306 306 else:
307 307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 308 c.comments = c.comment_versions[c.at_version_num]['until']
309 309
310 310 return general_comments, inline_comments
311 311
312 312 @LoginRequired()
313 313 @HasRepoPermissionAnyDecorator(
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 def pull_request_show(self):
316 316 _ = self.request.translate
317 317 c = self.load_default_context()
318 318
319 319 pull_request = PullRequest.get_or_404(
320 320 self.request.matchdict['pull_request_id'])
321 321 pull_request_id = pull_request.pull_request_id
322 322
323 323 c.state_progressing = pull_request.is_state_changing()
324 324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325 325
326 326 _new_state = {
327 327 'created': PullRequest.STATE_CREATED,
328 328 }.get(self.request.GET.get('force_state'))
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329 330
330 if c.is_super_admin and _new_state:
331 if can_force_state and _new_state:
331 332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 333 h.flash(
333 334 _('Pull Request state was force changed to `{}`').format(_new_state),
334 335 category='success')
335 336 Session().commit()
336 337
337 338 raise HTTPFound(h.route_path(
338 339 'pullrequest_show', repo_name=self.db_repo_name,
339 340 pull_request_id=pull_request_id))
340 341
341 342 version = self.request.GET.get('version')
342 343 from_version = self.request.GET.get('from_version') or version
343 344 merge_checks = self.request.GET.get('merge_checks')
344 345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347 348
348 349 # fetch global flags of ignore ws or context lines
349 350 diff_context = diffs.get_diff_context(self.request)
350 351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351 352
352 353 (pull_request_latest,
353 354 pull_request_at_ver,
354 355 pull_request_display_obj,
355 356 at_version) = PullRequestModel().get_pr_version(
356 357 pull_request_id, version=version)
357 358
358 359 pr_closed = pull_request_latest.is_closed()
359 360
360 361 if pr_closed and (version or from_version):
361 362 # not allow to browse versions for closed PR
362 363 raise HTTPFound(h.route_path(
363 364 'pullrequest_show', repo_name=self.db_repo_name,
364 365 pull_request_id=pull_request_id))
365 366
366 367 versions = pull_request_display_obj.versions()
367 368
368 369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369 370
370 371 # used to store per-commit range diffs
371 372 c.changes = collections.OrderedDict()
372 373
373 374 c.at_version = at_version
374 375 c.at_version_num = (at_version
375 376 if at_version and at_version != PullRequest.LATEST_VER
376 377 else None)
377 378
378 379 c.at_version_index = ChangesetComment.get_index_from_version(
379 380 c.at_version_num, versions)
380 381
381 382 (prev_pull_request_latest,
382 383 prev_pull_request_at_ver,
383 384 prev_pull_request_display_obj,
384 385 prev_at_version) = PullRequestModel().get_pr_version(
385 386 pull_request_id, version=from_version)
386 387
387 388 c.from_version = prev_at_version
388 389 c.from_version_num = (prev_at_version
389 390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 391 else None)
391 392 c.from_version_index = ChangesetComment.get_index_from_version(
392 393 c.from_version_num, versions)
393 394
394 395 # define if we're in COMPARE mode or VIEW at version mode
395 396 compare = at_version != prev_at_version
396 397
397 398 # pull_requests repo_name we opened it against
398 399 # ie. target_repo must match
399 400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 401 log.warning('Mismatch between the current repo: %s, and target %s',
401 402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 403 raise HTTPNotFound()
403 404
404 405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405 406
406 407 c.pull_request = pull_request_display_obj
407 408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 409 c.pull_request_latest = pull_request_latest
409 410
410 411 # inject latest version
411 412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 413 c.versions = versions + [latest_ver]
413 414
414 415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 416 c.allowed_to_change_status = False
416 417 c.allowed_to_update = False
417 418 c.allowed_to_merge = False
418 419 c.allowed_to_delete = False
419 420 c.allowed_to_comment = False
420 421 c.allowed_to_close = False
421 422 else:
422 423 can_change_status = PullRequestModel().check_user_change_status(
423 424 pull_request_at_ver, self._rhodecode_user)
424 425 c.allowed_to_change_status = can_change_status and not pr_closed
425 426
426 427 c.allowed_to_update = PullRequestModel().check_user_update(
427 428 pull_request_latest, self._rhodecode_user) and not pr_closed
428 429 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 430 pull_request_latest, self._rhodecode_user) and not pr_closed
430 431 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 432 pull_request_latest, self._rhodecode_user) and not pr_closed
432 433 c.allowed_to_comment = not pr_closed
433 434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434 435
435 436 c.forbid_adding_reviewers = False
436 437
437 438 if pull_request_latest.reviewer_data and \
438 439 'rules' in pull_request_latest.reviewer_data:
439 440 rules = pull_request_latest.reviewer_data['rules'] or {}
440 441 try:
441 442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 443 except Exception:
443 444 pass
444 445
445 446 # check merge capabilities
446 447 _merge_check = MergeCheck.validate(
447 448 pull_request_latest, auth_user=self._rhodecode_user,
448 449 translator=self.request.translate,
449 450 force_shadow_repo_refresh=force_refresh)
450 451
451 452 c.pr_merge_errors = _merge_check.error_details
452 453 c.pr_merge_possible = not _merge_check.failed
453 454 c.pr_merge_message = _merge_check.merge_msg
454 455 c.pr_merge_source_commit = _merge_check.source_commit
455 456 c.pr_merge_target_commit = _merge_check.target_commit
456 457
457 458 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 459 pull_request_latest, translator=self.request.translate)
459 460
460 461 c.pull_request_review_status = _merge_check.review_status
461 462 if merge_checks:
462 463 self.request.override_renderer = \
463 464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 465 return self._get_template_context(c)
465 466
466 467 c.reviewers_count = pull_request.reviewers_count
467 468 c.observers_count = pull_request.observers_count
468 469
469 470 # reviewers and statuses
470 471 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
471 472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473 474
474 475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 476 member_reviewer = h.reviewer_as_json(
476 477 member, reasons=reasons, mandatory=mandatory,
477 478 role=review_obj.role,
478 479 user_group=review_obj.rule_user_group_data()
479 480 )
480 481
481 482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 483 member_reviewer['review_status'] = current_review_status
483 484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 485 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486 487
487 488 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
488 489
489 490 for observer_obj, member in pull_request_at_ver.observers():
490 491 member_observer = h.reviewer_as_json(
491 492 member, reasons=[], mandatory=False,
492 493 role=observer_obj.role,
493 494 user_group=observer_obj.rule_user_group_data()
494 495 )
495 496 member_observer['allowed_to_update'] = c.allowed_to_update
496 497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 498
498 499 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
499 500
500 501 general_comments, inline_comments = \
501 502 self.register_comments_vars(c, pull_request_latest, versions)
502 503
503 504 # TODOs
504 505 c.unresolved_comments = CommentsModel() \
505 506 .get_pull_request_unresolved_todos(pull_request_latest)
506 507 c.resolved_comments = CommentsModel() \
507 508 .get_pull_request_resolved_todos(pull_request_latest)
508 509
509 510 # Drafts
510 511 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 512 self._rhodecode_db_user.user_id,
512 513 pull_request_latest)
513 514
514 515 # if we use version, then do not show later comments
515 516 # than current version
516 517 display_inline_comments = collections.defaultdict(
517 518 lambda: collections.defaultdict(list))
518 519 for co in inline_comments:
519 520 if c.at_version_num:
520 521 # pick comments that are at least UPTO given version, so we
521 522 # don't render comments for higher version
522 523 should_render = co.pull_request_version_id and \
523 524 co.pull_request_version_id <= c.at_version_num
524 525 else:
525 526 # showing all, for 'latest'
526 527 should_render = True
527 528
528 529 if should_render:
529 530 display_inline_comments[co.f_path][co.line_no].append(co)
530 531
531 532 # load diff data into template context, if we use compare mode then
532 533 # diff is calculated based on changes between versions of PR
533 534
534 535 source_repo = pull_request_at_ver.source_repo
535 536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 537
537 538 target_repo = pull_request_at_ver.target_repo
538 539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 540
540 541 if compare:
541 542 # in compare switch the diff base to latest commit from prev version
542 543 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 544
544 545 # despite opening commits for bookmarks/branches/tags, we always
545 546 # convert this to rev to prevent changes after bookmark or branch change
546 547 c.source_ref_type = 'rev'
547 548 c.source_ref = source_ref_id
548 549
549 550 c.target_ref_type = 'rev'
550 551 c.target_ref = target_ref_id
551 552
552 553 c.source_repo = source_repo
553 554 c.target_repo = target_repo
554 555
555 556 c.commit_ranges = []
556 557 source_commit = EmptyCommit()
557 558 target_commit = EmptyCommit()
558 559 c.missing_requirements = False
559 560
560 561 source_scm = source_repo.scm_instance()
561 562 target_scm = target_repo.scm_instance()
562 563
563 564 shadow_scm = None
564 565 try:
565 566 shadow_scm = pull_request_latest.get_shadow_repo()
566 567 except Exception:
567 568 log.debug('Failed to get shadow repo', exc_info=True)
568 569 # try first the existing source_repo, and then shadow
569 570 # repo if we can obtain one
570 571 commits_source_repo = source_scm
571 572 if shadow_scm:
572 573 commits_source_repo = shadow_scm
573 574
574 575 c.commits_source_repo = commits_source_repo
575 576 c.ancestor = None # set it to None, to hide it from PR view
576 577
577 578 # empty version means latest, so we keep this to prevent
578 579 # double caching
579 580 version_normalized = version or PullRequest.LATEST_VER
580 581 from_version_normalized = from_version or PullRequest.LATEST_VER
581 582
582 583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 584 cache_file_path = diff_cache_exist(
584 585 cache_path, 'pull_request', pull_request_id, version_normalized,
585 586 from_version_normalized, source_ref_id, target_ref_id,
586 587 hide_whitespace_changes, diff_context, c.fulldiff)
587 588
588 589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 590 force_recache = self.get_recache_flag()
590 591
591 592 cached_diff = None
592 593 if caching_enabled:
593 594 cached_diff = load_cached_diff(cache_file_path)
594 595
595 596 has_proper_commit_cache = (
596 597 cached_diff and cached_diff.get('commits')
597 598 and len(cached_diff.get('commits', [])) == 5
598 599 and cached_diff.get('commits')[0]
599 600 and cached_diff.get('commits')[3])
600 601
601 602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 603 diff_commit_cache = \
603 604 (ancestor_commit, commit_cache, missing_requirements,
604 605 source_commit, target_commit) = cached_diff['commits']
605 606 else:
606 607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 608 # merge errors resulting in potentially hidden commits in the shadow repo.
608 609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 610 and _merge_check.merge_response
610 611 maybe_unreachable = maybe_unreachable \
611 612 and _merge_check.merge_response.metadata.get('unresolved_files')
612 613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 614 diff_commit_cache = \
614 615 (ancestor_commit, commit_cache, missing_requirements,
615 616 source_commit, target_commit) = self.get_commits(
616 617 commits_source_repo,
617 618 pull_request_at_ver,
618 619 source_commit,
619 620 source_ref_id,
620 621 source_scm,
621 622 target_commit,
622 623 target_ref_id,
623 624 target_scm,
624 625 maybe_unreachable=maybe_unreachable)
625 626
626 627 # register our commit range
627 628 for comm in commit_cache.values():
628 629 c.commit_ranges.append(comm)
629 630
630 631 c.missing_requirements = missing_requirements
631 632 c.ancestor_commit = ancestor_commit
632 633 c.statuses = source_repo.statuses(
633 634 [x.raw_id for x in c.commit_ranges])
634 635
635 636 # auto collapse if we have more than limit
636 637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 639 c.compare_mode = compare
639 640
640 641 # diff_limit is the old behavior, will cut off the whole diff
641 642 # if the limit is applied otherwise will just hide the
642 643 # big files from the front-end
643 644 diff_limit = c.visual.cut_off_limit_diff
644 645 file_limit = c.visual.cut_off_limit_file
645 646
646 647 c.missing_commits = False
647 648 if (c.missing_requirements
648 649 or isinstance(source_commit, EmptyCommit)
649 650 or source_commit == target_commit):
650 651
651 652 c.missing_commits = True
652 653 else:
653 654 c.inline_comments = display_inline_comments
654 655
655 656 use_ancestor = True
656 657 if from_version_normalized != version_normalized:
657 658 use_ancestor = False
658 659
659 660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 661 if not force_recache and has_proper_diff_cache:
661 662 c.diffset = cached_diff['diff']
662 663 else:
663 664 try:
664 665 c.diffset = self._get_diffset(
665 666 c.source_repo.repo_name, commits_source_repo,
666 667 c.ancestor_commit,
667 668 source_ref_id, target_ref_id,
668 669 target_commit, source_commit,
669 670 diff_limit, file_limit, c.fulldiff,
670 671 hide_whitespace_changes, diff_context,
671 672 use_ancestor=use_ancestor
672 673 )
673 674
674 675 # save cached diff
675 676 if caching_enabled:
676 677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 678 except CommitDoesNotExistError:
678 679 log.exception('Failed to generate diffset')
679 680 c.missing_commits = True
680 681
681 682 if not c.missing_commits:
682 683
683 684 c.limited_diff = c.diffset.limited_diff
684 685
685 686 # calculate removed files that are bound to comments
686 687 comment_deleted_files = [
687 688 fname for fname in display_inline_comments
688 689 if fname not in c.diffset.file_stats]
689 690
690 691 c.deleted_files_comments = collections.defaultdict(dict)
691 692 for fname, per_line_comments in display_inline_comments.items():
692 693 if fname in comment_deleted_files:
693 694 c.deleted_files_comments[fname]['stats'] = 0
694 695 c.deleted_files_comments[fname]['comments'] = list()
695 696 for lno, comments in per_line_comments.items():
696 697 c.deleted_files_comments[fname]['comments'].extend(comments)
697 698
698 699 # maybe calculate the range diff
699 700 if c.range_diff_on:
700 701 # TODO(marcink): set whitespace/context
701 702 context_lcl = 3
702 703 ign_whitespace_lcl = False
703 704
704 705 for commit in c.commit_ranges:
705 706 commit2 = commit
706 707 commit1 = commit.first_parent
707 708
708 709 range_diff_cache_file_path = diff_cache_exist(
709 710 cache_path, 'diff', commit.raw_id,
710 711 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 712
712 713 cached_diff = None
713 714 if caching_enabled:
714 715 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 716
716 717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 718 if not force_recache and has_proper_diff_cache:
718 719 diffset = cached_diff['diff']
719 720 else:
720 721 diffset = self._get_range_diffset(
721 722 commits_source_repo, source_repo,
722 723 commit1, commit2, diff_limit, file_limit,
723 724 c.fulldiff, ign_whitespace_lcl, context_lcl
724 725 )
725 726
726 727 # save cached diff
727 728 if caching_enabled:
728 729 cache_diff(range_diff_cache_file_path, diffset, None)
729 730
730 731 c.changes[commit.raw_id] = diffset
731 732
732 733 # this is a hack to properly display links, when creating PR, the
733 734 # compare view and others uses different notation, and
734 735 # compare_commits.mako renders links based on the target_repo.
735 736 # We need to swap that here to generate it properly on the html side
736 737 c.target_repo = c.source_repo
737 738
738 739 c.commit_statuses = ChangesetStatus.STATUSES
739 740
740 741 c.show_version_changes = not pr_closed
741 742 if c.show_version_changes:
742 743 cur_obj = pull_request_at_ver
743 744 prev_obj = prev_pull_request_at_ver
744 745
745 746 old_commit_ids = prev_obj.revisions
746 747 new_commit_ids = cur_obj.revisions
747 748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 749 old_commit_ids, new_commit_ids)
749 750 c.commit_changes_summary = commit_changes
750 751
751 752 # calculate the diff for commits between versions
752 753 c.commit_changes = []
753 754
754 755 def mark(cs, fw):
755 756 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
756 757
757 758 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 759 + mark(commit_changes.removed, 'r') \
759 760 + mark(commit_changes.common, 'c'):
760 761
761 762 if raw_id in commit_cache:
762 763 commit = commit_cache[raw_id]
763 764 else:
764 765 try:
765 766 commit = commits_source_repo.get_commit(raw_id)
766 767 except CommitDoesNotExistError:
767 768 # in case we fail extracting still use "dummy" commit
768 769 # for display in commit diff
769 770 commit = h.AttributeDict(
770 771 {'raw_id': raw_id,
771 772 'message': 'EMPTY or MISSING COMMIT'})
772 773 c.commit_changes.append([c_type, commit])
773 774
774 775 # current user review statuses for each version
775 776 c.review_versions = {}
776 777 is_reviewer = PullRequestModel().is_user_reviewer(
777 778 pull_request, self._rhodecode_user)
778 779 if is_reviewer:
779 780 for co in general_comments:
780 781 if co.author.user_id == self._rhodecode_user.user_id:
781 782 status = co.status_change
782 783 if status:
783 784 _ver_pr = status[0].comment.pull_request_version_id
784 785 c.review_versions[_ver_pr] = status[0]
785 786
786 787 return self._get_template_context(c)
787 788
788 789 def get_commits(
789 790 self, commits_source_repo, pull_request_at_ver, source_commit,
790 791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 792 maybe_unreachable=False):
792 793
793 794 commit_cache = collections.OrderedDict()
794 795 missing_requirements = False
795 796
796 797 try:
797 798 pre_load = ["author", "date", "message", "branch", "parents"]
798 799
799 800 pull_request_commits = pull_request_at_ver.revisions
800 801 log.debug('Loading %s commits from %s',
801 802 len(pull_request_commits), commits_source_repo)
802 803
803 804 for rev in pull_request_commits:
804 805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 806 maybe_unreachable=maybe_unreachable)
806 807 commit_cache[comm.raw_id] = comm
807 808
808 809 # Order here matters, we first need to get target, and then
809 810 # the source
810 811 target_commit = commits_source_repo.get_commit(
811 812 commit_id=safe_str(target_ref_id))
812 813
813 814 source_commit = commits_source_repo.get_commit(
814 815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 816 except CommitDoesNotExistError:
816 817 log.warning('Failed to get commit from `{}` repo'.format(
817 818 commits_source_repo), exc_info=True)
818 819 except RepositoryRequirementError:
819 820 log.warning('Failed to get all required data from repo', exc_info=True)
820 821 missing_requirements = True
821 822
822 823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 824
824 825 try:
825 826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 827 except Exception:
827 828 ancestor_commit = None
828 829
829 830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 831
831 832 def assure_not_empty_repo(self):
832 833 _ = self.request.translate
833 834
834 835 try:
835 836 self.db_repo.scm_instance().get_commit()
836 837 except EmptyRepositoryError:
837 838 h.flash(h.literal(_('There are no commits yet')),
838 839 category='warning')
839 840 raise HTTPFound(
840 841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 842
842 843 @LoginRequired()
843 844 @NotAnonymous()
844 845 @HasRepoPermissionAnyDecorator(
845 846 'repository.read', 'repository.write', 'repository.admin')
846 847 def pull_request_new(self):
847 848 _ = self.request.translate
848 849 c = self.load_default_context()
849 850
850 851 self.assure_not_empty_repo()
851 852 source_repo = self.db_repo
852 853
853 854 commit_id = self.request.GET.get('commit')
854 855 branch_ref = self.request.GET.get('branch')
855 856 bookmark_ref = self.request.GET.get('bookmark')
856 857
857 858 try:
858 859 source_repo_data = PullRequestModel().generate_repo_data(
859 860 source_repo, commit_id=commit_id,
860 861 branch=branch_ref, bookmark=bookmark_ref,
861 862 translator=self.request.translate)
862 863 except CommitDoesNotExistError as e:
863 864 log.exception(e)
864 865 h.flash(_('Commit does not exist'), 'error')
865 866 raise HTTPFound(
866 867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 868
868 869 default_target_repo = source_repo
869 870
870 871 if source_repo.parent and c.has_origin_repo_read_perm:
871 872 parent_vcs_obj = source_repo.parent.scm_instance()
872 873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 874 # change default if we have a parent repo
874 875 default_target_repo = source_repo.parent
875 876
876 877 target_repo_data = PullRequestModel().generate_repo_data(
877 878 default_target_repo, translator=self.request.translate)
878 879
879 880 selected_source_ref = source_repo_data['refs']['selected_ref']
880 881 title_source_ref = ''
881 882 if selected_source_ref:
882 883 title_source_ref = selected_source_ref.split(':', 2)[1]
883 884 c.default_title = PullRequestModel().generate_pullrequest_title(
884 885 source=source_repo.repo_name,
885 886 source_ref=title_source_ref,
886 887 target=default_target_repo.repo_name
887 888 )
888 889
889 890 c.default_repo_data = {
890 891 'source_repo_name': source_repo.repo_name,
891 892 'source_refs_json': json.dumps(source_repo_data),
892 893 'target_repo_name': default_target_repo.repo_name,
893 894 'target_refs_json': json.dumps(target_repo_data),
894 895 }
895 896 c.default_source_ref = selected_source_ref
896 897
897 898 return self._get_template_context(c)
898 899
899 900 @LoginRequired()
900 901 @NotAnonymous()
901 902 @HasRepoPermissionAnyDecorator(
902 903 'repository.read', 'repository.write', 'repository.admin')
903 904 def pull_request_repo_refs(self):
904 905 self.load_default_context()
905 906 target_repo_name = self.request.matchdict['target_repo_name']
906 907 repo = Repository.get_by_repo_name(target_repo_name)
907 908 if not repo:
908 909 raise HTTPNotFound()
909 910
910 911 target_perm = HasRepoPermissionAny(
911 912 'repository.read', 'repository.write', 'repository.admin')(
912 913 target_repo_name)
913 914 if not target_perm:
914 915 raise HTTPNotFound()
915 916
916 917 return PullRequestModel().generate_repo_data(
917 918 repo, translator=self.request.translate)
918 919
919 920 @LoginRequired()
920 921 @NotAnonymous()
921 922 @HasRepoPermissionAnyDecorator(
922 923 'repository.read', 'repository.write', 'repository.admin')
923 924 def pullrequest_repo_targets(self):
924 925 _ = self.request.translate
925 926 filter_query = self.request.GET.get('query')
926 927
927 928 # get the parents
928 929 parent_target_repos = []
929 930 if self.db_repo.parent:
930 931 parents_query = Repository.query() \
931 932 .order_by(func.length(Repository.repo_name)) \
932 933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 934
934 935 if filter_query:
935 936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 937 parents_query = parents_query.filter(
937 938 Repository.repo_name.ilike(ilike_expression))
938 939 parents = parents_query.limit(20).all()
939 940
940 941 for parent in parents:
941 942 parent_vcs_obj = parent.scm_instance()
942 943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 944 parent_target_repos.append(parent)
944 945
945 946 # get other forks, and repo itself
946 947 query = Repository.query() \
947 948 .order_by(func.length(Repository.repo_name)) \
948 949 .filter(
949 950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 952 ) \
952 953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 954
954 955 if filter_query:
955 956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 958
958 959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 960 target_repos = query.limit(limit).all()
960 961
961 962 all_target_repos = target_repos + parent_target_repos
962 963
963 964 repos = []
964 965 # This checks permissions to the repositories
965 966 for obj in ScmModel().get_repos(all_target_repos):
966 967 repos.append({
967 968 'id': obj['name'],
968 969 'text': obj['name'],
969 970 'type': 'repo',
970 971 'repo_id': obj['dbrepo']['repo_id'],
971 972 'repo_type': obj['dbrepo']['repo_type'],
972 973 'private': obj['dbrepo']['private'],
973 974
974 975 })
975 976
976 977 data = {
977 978 'more': False,
978 979 'results': [{
979 980 'text': _('Repositories'),
980 981 'children': repos
981 982 }] if repos else []
982 983 }
983 984 return data
984 985
985 986 @classmethod
986 987 def get_comment_ids(cls, post_data):
987 988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 989
989 990 @LoginRequired()
990 991 @NotAnonymous()
991 992 @HasRepoPermissionAnyDecorator(
992 993 'repository.read', 'repository.write', 'repository.admin')
993 994 def pullrequest_comments(self):
994 995 self.load_default_context()
995 996
996 997 pull_request = PullRequest.get_or_404(
997 998 self.request.matchdict['pull_request_id'])
998 999 pull_request_id = pull_request.pull_request_id
999 1000 version = self.request.GET.get('version')
1000 1001
1001 1002 _render = self.request.get_partial_renderer(
1002 1003 'rhodecode:templates/base/sidebar.mako')
1003 1004 c = _render.get_call_context()
1004 1005
1005 1006 (pull_request_latest,
1006 1007 pull_request_at_ver,
1007 1008 pull_request_display_obj,
1008 1009 at_version) = PullRequestModel().get_pr_version(
1009 1010 pull_request_id, version=version)
1010 1011 versions = pull_request_display_obj.versions()
1011 1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 1013 c.versions = versions + [latest_ver]
1013 1014
1014 1015 c.at_version = at_version
1015 1016 c.at_version_num = (at_version
1016 1017 if at_version and at_version != PullRequest.LATEST_VER
1017 1018 else None)
1018 1019
1019 1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 1021 all_comments = c.inline_comments_flat + c.comments
1021 1022
1022 1023 existing_ids = self.get_comment_ids(self.request.POST)
1023 1024 return _render('comments_table', all_comments, len(all_comments),
1024 1025 existing_ids=existing_ids)
1025 1026
1026 1027 @LoginRequired()
1027 1028 @NotAnonymous()
1028 1029 @HasRepoPermissionAnyDecorator(
1029 1030 'repository.read', 'repository.write', 'repository.admin')
1030 1031 def pullrequest_todos(self):
1031 1032 self.load_default_context()
1032 1033
1033 1034 pull_request = PullRequest.get_or_404(
1034 1035 self.request.matchdict['pull_request_id'])
1035 1036 pull_request_id = pull_request.pull_request_id
1036 1037 version = self.request.GET.get('version')
1037 1038
1038 1039 _render = self.request.get_partial_renderer(
1039 1040 'rhodecode:templates/base/sidebar.mako')
1040 1041 c = _render.get_call_context()
1041 1042 (pull_request_latest,
1042 1043 pull_request_at_ver,
1043 1044 pull_request_display_obj,
1044 1045 at_version) = PullRequestModel().get_pr_version(
1045 1046 pull_request_id, version=version)
1046 1047 versions = pull_request_display_obj.versions()
1047 1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 1049 c.versions = versions + [latest_ver]
1049 1050
1050 1051 c.at_version = at_version
1051 1052 c.at_version_num = (at_version
1052 1053 if at_version and at_version != PullRequest.LATEST_VER
1053 1054 else None)
1054 1055
1055 1056 c.unresolved_comments = CommentsModel() \
1056 1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 1058 c.resolved_comments = CommentsModel() \
1058 1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 1060
1060 1061 all_comments = c.unresolved_comments + c.resolved_comments
1061 1062 existing_ids = self.get_comment_ids(self.request.POST)
1062 1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 1064 todo_comments=True, existing_ids=existing_ids)
1064 1065
1065 1066 @LoginRequired()
1066 1067 @NotAnonymous()
1067 1068 @HasRepoPermissionAnyDecorator(
1068 1069 'repository.read', 'repository.write', 'repository.admin')
1069 1070 def pullrequest_drafts(self):
1070 1071 self.load_default_context()
1071 1072
1072 1073 pull_request = PullRequest.get_or_404(
1073 1074 self.request.matchdict['pull_request_id'])
1074 1075 pull_request_id = pull_request.pull_request_id
1075 1076 version = self.request.GET.get('version')
1076 1077
1077 1078 _render = self.request.get_partial_renderer(
1078 1079 'rhodecode:templates/base/sidebar.mako')
1079 1080 c = _render.get_call_context()
1080 1081
1081 1082 (pull_request_latest,
1082 1083 pull_request_at_ver,
1083 1084 pull_request_display_obj,
1084 1085 at_version) = PullRequestModel().get_pr_version(
1085 1086 pull_request_id, version=version)
1086 1087 versions = pull_request_display_obj.versions()
1087 1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 1089 c.versions = versions + [latest_ver]
1089 1090
1090 1091 c.at_version = at_version
1091 1092 c.at_version_num = (at_version
1092 1093 if at_version and at_version != PullRequest.LATEST_VER
1093 1094 else None)
1094 1095
1095 1096 c.draft_comments = CommentsModel() \
1096 1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 1098
1098 1099 all_comments = c.draft_comments
1099 1100
1100 1101 existing_ids = self.get_comment_ids(self.request.POST)
1101 1102 return _render('comments_table', all_comments, len(all_comments),
1102 1103 existing_ids=existing_ids, draft_comments=True)
1103 1104
1104 1105 @LoginRequired()
1105 1106 @NotAnonymous()
1106 1107 @HasRepoPermissionAnyDecorator(
1107 1108 'repository.read', 'repository.write', 'repository.admin')
1108 1109 @CSRFRequired()
1109 1110 def pull_request_create(self):
1110 1111 _ = self.request.translate
1111 1112 self.assure_not_empty_repo()
1112 1113 self.load_default_context()
1113 1114
1114 1115 controls = peppercorn.parse(self.request.POST.items())
1115 1116
1116 1117 try:
1117 1118 form = PullRequestForm(
1118 1119 self.request.translate, self.db_repo.repo_id)()
1119 1120 _form = form.to_python(controls)
1120 1121 except formencode.Invalid as errors:
1121 1122 if errors.error_dict.get('revisions'):
1122 1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 1124 elif errors.error_dict.get('pullrequest_title'):
1124 1125 msg = errors.error_dict.get('pullrequest_title')
1125 1126 else:
1126 1127 msg = _('Error creating pull request: {}').format(errors)
1127 1128 log.exception(msg)
1128 1129 h.flash(msg, 'error')
1129 1130
1130 1131 # would rather just go back to form ...
1131 1132 raise HTTPFound(
1132 1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 1134
1134 1135 source_repo = _form['source_repo']
1135 1136 source_ref = _form['source_ref']
1136 1137 target_repo = _form['target_repo']
1137 1138 target_ref = _form['target_ref']
1138 1139 commit_ids = _form['revisions'][::-1]
1139 1140 common_ancestor_id = _form['common_ancestor']
1140 1141
1141 1142 # find the ancestor for this pr
1142 1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 1145
1145 1146 if not (source_db_repo or target_db_repo):
1146 1147 h.flash(_('source_repo or target repo not found'), category='error')
1147 1148 raise HTTPFound(
1148 1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 1150
1150 1151 # re-check permissions again here
1151 1152 # source_repo we must have read permissions
1152 1153
1153 1154 source_perm = HasRepoPermissionAny(
1154 1155 'repository.read', 'repository.write', 'repository.admin')(
1155 1156 source_db_repo.repo_name)
1156 1157 if not source_perm:
1157 1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 1159 source_db_repo.repo_name))
1159 1160 h.flash(msg, category='error')
1160 1161 # copy the args back to redirect
1161 1162 org_query = self.request.GET.mixed()
1162 1163 raise HTTPFound(
1163 1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 1165 _query=org_query))
1165 1166
1166 1167 # target repo we must have read permissions, and also later on
1167 1168 # we want to check branch permissions here
1168 1169 target_perm = HasRepoPermissionAny(
1169 1170 'repository.read', 'repository.write', 'repository.admin')(
1170 1171 target_db_repo.repo_name)
1171 1172 if not target_perm:
1172 1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 1174 target_db_repo.repo_name))
1174 1175 h.flash(msg, category='error')
1175 1176 # copy the args back to redirect
1176 1177 org_query = self.request.GET.mixed()
1177 1178 raise HTTPFound(
1178 1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 1180 _query=org_query))
1180 1181
1181 1182 source_scm = source_db_repo.scm_instance()
1182 1183 target_scm = target_db_repo.scm_instance()
1183 1184
1184 1185 source_ref_obj = unicode_to_reference(source_ref)
1185 1186 target_ref_obj = unicode_to_reference(target_ref)
1186 1187
1187 1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 1190
1190 1191 ancestor = source_scm.get_common_ancestor(
1191 1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 1193
1193 1194 # recalculate target ref based on ancestor
1194 1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 1196
1196 1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 1198 PullRequestModel().get_reviewer_functions()
1198 1199
1199 1200 # recalculate reviewers logic, to make sure we can validate this
1200 1201 reviewer_rules = get_default_reviewers_data(
1201 1202 self._rhodecode_db_user,
1202 1203 source_db_repo,
1203 1204 source_ref_obj,
1204 1205 target_db_repo,
1205 1206 target_ref_obj,
1206 1207 include_diff_info=False)
1207 1208
1208 1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 1211
1211 1212 pullrequest_title = _form['pullrequest_title']
1212 1213 title_source_ref = source_ref_obj.name
1213 1214 if not pullrequest_title:
1214 1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 1216 source=source_repo,
1216 1217 source_ref=title_source_ref,
1217 1218 target=target_repo
1218 1219 )
1219 1220
1220 1221 description = _form['pullrequest_desc']
1221 1222 description_renderer = _form['description_renderer']
1222 1223
1223 1224 try:
1224 1225 pull_request = PullRequestModel().create(
1225 1226 created_by=self._rhodecode_user.user_id,
1226 1227 source_repo=source_repo,
1227 1228 source_ref=source_ref,
1228 1229 target_repo=target_repo,
1229 1230 target_ref=target_ref,
1230 1231 revisions=commit_ids,
1231 1232 common_ancestor_id=common_ancestor_id,
1232 1233 reviewers=reviewers,
1233 1234 observers=observers,
1234 1235 title=pullrequest_title,
1235 1236 description=description,
1236 1237 description_renderer=description_renderer,
1237 1238 reviewer_data=reviewer_rules,
1238 1239 auth_user=self._rhodecode_user
1239 1240 )
1240 1241 Session().commit()
1241 1242
1242 1243 h.flash(_('Successfully opened new pull request'),
1243 1244 category='success')
1244 1245 except Exception:
1245 1246 msg = _('Error occurred during creation of this pull request.')
1246 1247 log.exception(msg)
1247 1248 h.flash(msg, category='error')
1248 1249
1249 1250 # copy the args back to redirect
1250 1251 org_query = self.request.GET.mixed()
1251 1252 raise HTTPFound(
1252 1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 1254 _query=org_query))
1254 1255
1255 1256 raise HTTPFound(
1256 1257 h.route_path('pullrequest_show', repo_name=target_repo,
1257 1258 pull_request_id=pull_request.pull_request_id))
1258 1259
1259 1260 @LoginRequired()
1260 1261 @NotAnonymous()
1261 1262 @HasRepoPermissionAnyDecorator(
1262 1263 'repository.read', 'repository.write', 'repository.admin')
1263 1264 @CSRFRequired()
1264 1265 def pull_request_update(self):
1265 1266 pull_request = PullRequest.get_or_404(
1266 1267 self.request.matchdict['pull_request_id'])
1267 1268 _ = self.request.translate
1268 1269
1269 1270 c = self.load_default_context()
1270 1271 redirect_url = None
1271 1272
1272 1273 if pull_request.is_closed():
1273 1274 log.debug('update: forbidden because pull request is closed')
1274 1275 msg = _(u'Cannot update closed pull requests.')
1275 1276 h.flash(msg, category='error')
1276 1277 return {'response': True,
1277 1278 'redirect_url': redirect_url}
1278 1279
1279 1280 is_state_changing = pull_request.is_state_changing()
1280 1281 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281 1282
1282 1283 # only owner or admin can update it
1283 1284 allowed_to_update = PullRequestModel().check_user_update(
1284 1285 pull_request, self._rhodecode_user)
1285 1286
1286 1287 if allowed_to_update:
1287 1288 controls = peppercorn.parse(self.request.POST.items())
1288 1289 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1289 1290
1290 1291 if 'review_members' in controls:
1291 1292 self._update_reviewers(
1292 1293 c,
1293 1294 pull_request, controls['review_members'],
1294 1295 pull_request.reviewer_data,
1295 1296 PullRequestReviewers.ROLE_REVIEWER)
1296 1297 elif 'observer_members' in controls:
1297 1298 self._update_reviewers(
1298 1299 c,
1299 1300 pull_request, controls['observer_members'],
1300 1301 pull_request.reviewer_data,
1301 1302 PullRequestReviewers.ROLE_OBSERVER)
1302 1303 elif str2bool(self.request.POST.get('update_commits', 'false')):
1303 1304 if is_state_changing:
1304 1305 log.debug('commits update: forbidden because pull request is in state %s',
1305 1306 pull_request.pull_request_state)
1306 1307 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1307 1308 u'Current state is: `{}`').format(
1308 1309 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1309 1310 h.flash(msg, category='error')
1310 1311 return {'response': True,
1311 1312 'redirect_url': redirect_url}
1312 1313
1313 1314 self._update_commits(c, pull_request)
1314 1315 if force_refresh:
1315 1316 redirect_url = h.route_path(
1316 1317 'pullrequest_show', repo_name=self.db_repo_name,
1317 1318 pull_request_id=pull_request.pull_request_id,
1318 1319 _query={"force_refresh": 1})
1319 1320 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1320 1321 self._edit_pull_request(pull_request)
1321 1322 else:
1322 1323 log.error('Unhandled update data.')
1323 1324 raise HTTPBadRequest()
1324 1325
1325 1326 return {'response': True,
1326 1327 'redirect_url': redirect_url}
1327 1328 raise HTTPForbidden()
1328 1329
1329 1330 def _edit_pull_request(self, pull_request):
1330 1331 """
1331 1332 Edit title and description
1332 1333 """
1333 1334 _ = self.request.translate
1334 1335
1335 1336 try:
1336 1337 PullRequestModel().edit(
1337 1338 pull_request,
1338 1339 self.request.POST.get('title'),
1339 1340 self.request.POST.get('description'),
1340 1341 self.request.POST.get('description_renderer'),
1341 1342 self._rhodecode_user)
1342 1343 except ValueError:
1343 1344 msg = _(u'Cannot update closed pull requests.')
1344 1345 h.flash(msg, category='error')
1345 1346 return
1346 1347 else:
1347 1348 Session().commit()
1348 1349
1349 1350 msg = _(u'Pull request title & description updated.')
1350 1351 h.flash(msg, category='success')
1351 1352 return
1352 1353
1353 1354 def _update_commits(self, c, pull_request):
1354 1355 _ = self.request.translate
1355 1356
1356 1357 @retry(exception=Exception, n_tries=3)
1357 1358 def commits_update():
1358 1359 return PullRequestModel().update_commits(
1359 1360 pull_request, self._rhodecode_db_user)
1360 1361
1361 1362 with pull_request.set_state(PullRequest.STATE_UPDATING):
1362 1363 resp = commits_update() # retry x3
1363 1364
1364 1365 if resp.executed:
1365 1366
1366 1367 if resp.target_changed and resp.source_changed:
1367 1368 changed = 'target and source repositories'
1368 1369 elif resp.target_changed and not resp.source_changed:
1369 1370 changed = 'target repository'
1370 1371 elif not resp.target_changed and resp.source_changed:
1371 1372 changed = 'source repository'
1372 1373 else:
1373 1374 changed = 'nothing'
1374 1375
1375 1376 msg = _(u'Pull request updated to "{source_commit_id}" with '
1376 1377 u'{count_added} added, {count_removed} removed commits. '
1377 1378 u'Source of changes: {change_source}.')
1378 1379 msg = msg.format(
1379 1380 source_commit_id=pull_request.source_ref_parts.commit_id,
1380 1381 count_added=len(resp.changes.added),
1381 1382 count_removed=len(resp.changes.removed),
1382 1383 change_source=changed)
1383 1384 h.flash(msg, category='success')
1384 1385 channelstream.pr_update_channelstream_push(
1385 1386 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1386 1387 else:
1387 1388 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1388 1389 warning_reasons = [
1389 1390 UpdateFailureReason.NO_CHANGE,
1390 1391 UpdateFailureReason.WRONG_REF_TYPE,
1391 1392 ]
1392 1393 category = 'warning' if resp.reason in warning_reasons else 'error'
1393 1394 h.flash(msg, category=category)
1394 1395
1395 1396 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1396 1397 _ = self.request.translate
1397 1398
1398 1399 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1399 1400 PullRequestModel().get_reviewer_functions()
1400 1401
1401 1402 if role == PullRequestReviewers.ROLE_REVIEWER:
1402 1403 try:
1403 1404 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1404 1405 except ValueError as e:
1405 1406 log.error('Reviewers Validation: {}'.format(e))
1406 1407 h.flash(e, category='error')
1407 1408 return
1408 1409
1409 1410 old_calculated_status = pull_request.calculated_review_status()
1410 1411 PullRequestModel().update_reviewers(
1411 1412 pull_request, reviewers, self._rhodecode_db_user)
1412 1413
1413 1414 Session().commit()
1414 1415
1415 1416 msg = _('Pull request reviewers updated.')
1416 1417 h.flash(msg, category='success')
1417 1418 channelstream.pr_update_channelstream_push(
1418 1419 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1419 1420
1420 1421 # trigger status changed if change in reviewers changes the status
1421 1422 calculated_status = pull_request.calculated_review_status()
1422 1423 if old_calculated_status != calculated_status:
1423 1424 PullRequestModel().trigger_pull_request_hook(
1424 1425 pull_request, self._rhodecode_user, 'review_status_change',
1425 1426 data={'status': calculated_status})
1426 1427
1427 1428 elif role == PullRequestReviewers.ROLE_OBSERVER:
1428 1429 try:
1429 1430 observers = validate_observers(review_members, reviewer_rules)
1430 1431 except ValueError as e:
1431 1432 log.error('Observers Validation: {}'.format(e))
1432 1433 h.flash(e, category='error')
1433 1434 return
1434 1435
1435 1436 PullRequestModel().update_observers(
1436 1437 pull_request, observers, self._rhodecode_db_user)
1437 1438
1438 1439 Session().commit()
1439 1440 msg = _('Pull request observers updated.')
1440 1441 h.flash(msg, category='success')
1441 1442 channelstream.pr_update_channelstream_push(
1442 1443 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1443 1444
1444 1445 @LoginRequired()
1445 1446 @NotAnonymous()
1446 1447 @HasRepoPermissionAnyDecorator(
1447 1448 'repository.read', 'repository.write', 'repository.admin')
1448 1449 @CSRFRequired()
1449 1450 def pull_request_merge(self):
1450 1451 """
1451 1452 Merge will perform a server-side merge of the specified
1452 1453 pull request, if the pull request is approved and mergeable.
1453 1454 After successful merging, the pull request is automatically
1454 1455 closed, with a relevant comment.
1455 1456 """
1456 1457 pull_request = PullRequest.get_or_404(
1457 1458 self.request.matchdict['pull_request_id'])
1458 1459 _ = self.request.translate
1459 1460
1460 1461 if pull_request.is_state_changing():
1461 1462 log.debug('show: forbidden because pull request is in state %s',
1462 1463 pull_request.pull_request_state)
1463 1464 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1464 1465 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1465 1466 pull_request.pull_request_state)
1466 1467 h.flash(msg, category='error')
1467 1468 raise HTTPFound(
1468 1469 h.route_path('pullrequest_show',
1469 1470 repo_name=pull_request.target_repo.repo_name,
1470 1471 pull_request_id=pull_request.pull_request_id))
1471 1472
1472 1473 self.load_default_context()
1473 1474
1474 1475 with pull_request.set_state(PullRequest.STATE_UPDATING):
1475 1476 check = MergeCheck.validate(
1476 1477 pull_request, auth_user=self._rhodecode_user,
1477 1478 translator=self.request.translate)
1478 1479 merge_possible = not check.failed
1479 1480
1480 1481 for err_type, error_msg in check.errors:
1481 1482 h.flash(error_msg, category=err_type)
1482 1483
1483 1484 if merge_possible:
1484 1485 log.debug("Pre-conditions checked, trying to merge.")
1485 1486 extras = vcs_operation_context(
1486 1487 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1487 1488 username=self._rhodecode_db_user.username, action='push',
1488 1489 scm=pull_request.target_repo.repo_type)
1489 1490 with pull_request.set_state(PullRequest.STATE_UPDATING):
1490 1491 self._merge_pull_request(
1491 1492 pull_request, self._rhodecode_db_user, extras)
1492 1493 else:
1493 1494 log.debug("Pre-conditions failed, NOT merging.")
1494 1495
1495 1496 raise HTTPFound(
1496 1497 h.route_path('pullrequest_show',
1497 1498 repo_name=pull_request.target_repo.repo_name,
1498 1499 pull_request_id=pull_request.pull_request_id))
1499 1500
1500 1501 def _merge_pull_request(self, pull_request, user, extras):
1501 1502 _ = self.request.translate
1502 1503 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1503 1504
1504 1505 if merge_resp.executed:
1505 1506 log.debug("The merge was successful, closing the pull request.")
1506 1507 PullRequestModel().close_pull_request(
1507 1508 pull_request.pull_request_id, user)
1508 1509 Session().commit()
1509 1510 msg = _('Pull request was successfully merged and closed.')
1510 1511 h.flash(msg, category='success')
1511 1512 else:
1512 1513 log.debug(
1513 1514 "The merge was not successful. Merge response: %s", merge_resp)
1514 1515 msg = merge_resp.merge_status_message
1515 1516 h.flash(msg, category='error')
1516 1517
1517 1518 @LoginRequired()
1518 1519 @NotAnonymous()
1519 1520 @HasRepoPermissionAnyDecorator(
1520 1521 'repository.read', 'repository.write', 'repository.admin')
1521 1522 @CSRFRequired()
1522 1523 def pull_request_delete(self):
1523 1524 _ = self.request.translate
1524 1525
1525 1526 pull_request = PullRequest.get_or_404(
1526 1527 self.request.matchdict['pull_request_id'])
1527 1528 self.load_default_context()
1528 1529
1529 1530 pr_closed = pull_request.is_closed()
1530 1531 allowed_to_delete = PullRequestModel().check_user_delete(
1531 1532 pull_request, self._rhodecode_user) and not pr_closed
1532 1533
1533 1534 # only owner can delete it !
1534 1535 if allowed_to_delete:
1535 1536 PullRequestModel().delete(pull_request, self._rhodecode_user)
1536 1537 Session().commit()
1537 1538 h.flash(_('Successfully deleted pull request'),
1538 1539 category='success')
1539 1540 raise HTTPFound(h.route_path('pullrequest_show_all',
1540 1541 repo_name=self.db_repo_name))
1541 1542
1542 1543 log.warning('user %s tried to delete pull request without access',
1543 1544 self._rhodecode_user)
1544 1545 raise HTTPNotFound()
1545 1546
1546 1547 def _pull_request_comments_create(self, pull_request, comments):
1547 1548 _ = self.request.translate
1548 1549 data = {}
1549 1550 if not comments:
1550 1551 return
1551 1552 pull_request_id = pull_request.pull_request_id
1552 1553
1553 1554 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1554 1555
1555 1556 for entry in comments:
1556 1557 c = self.load_default_context()
1557 1558 comment_type = entry['comment_type']
1558 1559 text = entry['text']
1559 1560 status = entry['status']
1560 1561 is_draft = str2bool(entry['is_draft'])
1561 1562 resolves_comment_id = entry['resolves_comment_id']
1562 1563 close_pull_request = entry['close_pull_request']
1563 1564 f_path = entry['f_path']
1564 1565 line_no = entry['line']
1565 1566 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1566 1567
1567 1568 # the logic here should work like following, if we submit close
1568 1569 # pr comment, use `close_pull_request_with_comment` function
1569 1570 # else handle regular comment logic
1570 1571
1571 1572 if close_pull_request:
1572 1573 # only owner or admin or person with write permissions
1573 1574 allowed_to_close = PullRequestModel().check_user_update(
1574 1575 pull_request, self._rhodecode_user)
1575 1576 if not allowed_to_close:
1576 1577 log.debug('comment: forbidden because not allowed to close '
1577 1578 'pull request %s', pull_request_id)
1578 1579 raise HTTPForbidden()
1579 1580
1580 1581 # This also triggers `review_status_change`
1581 1582 comment, status = PullRequestModel().close_pull_request_with_comment(
1582 1583 pull_request, self._rhodecode_user, self.db_repo, message=text,
1583 1584 auth_user=self._rhodecode_user)
1584 1585 Session().flush()
1585 1586 is_inline = comment.is_inline
1586 1587
1587 1588 PullRequestModel().trigger_pull_request_hook(
1588 1589 pull_request, self._rhodecode_user, 'comment',
1589 1590 data={'comment': comment})
1590 1591
1591 1592 else:
1592 1593 # regular comment case, could be inline, or one with status.
1593 1594 # for that one we check also permissions
1594 1595 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1595 1596 allowed_to_change_status = PullRequestModel().check_user_change_status(
1596 1597 pull_request, self._rhodecode_user) and not is_draft
1597 1598
1598 1599 if status and allowed_to_change_status:
1599 1600 message = (_('Status change %(transition_icon)s %(status)s')
1600 1601 % {'transition_icon': '>',
1601 1602 'status': ChangesetStatus.get_status_lbl(status)})
1602 1603 text = text or message
1603 1604
1604 1605 comment = CommentsModel().create(
1605 1606 text=text,
1606 1607 repo=self.db_repo.repo_id,
1607 1608 user=self._rhodecode_user.user_id,
1608 1609 pull_request=pull_request,
1609 1610 f_path=f_path,
1610 1611 line_no=line_no,
1611 1612 status_change=(ChangesetStatus.get_status_lbl(status)
1612 1613 if status and allowed_to_change_status else None),
1613 1614 status_change_type=(status
1614 1615 if status and allowed_to_change_status else None),
1615 1616 comment_type=comment_type,
1616 1617 is_draft=is_draft,
1617 1618 resolves_comment_id=resolves_comment_id,
1618 1619 auth_user=self._rhodecode_user,
1619 1620 send_email=not is_draft, # skip notification for draft comments
1620 1621 )
1621 1622 is_inline = comment.is_inline
1622 1623
1623 1624 if allowed_to_change_status:
1624 1625 # calculate old status before we change it
1625 1626 old_calculated_status = pull_request.calculated_review_status()
1626 1627
1627 1628 # get status if set !
1628 1629 if status:
1629 1630 ChangesetStatusModel().set_status(
1630 1631 self.db_repo.repo_id,
1631 1632 status,
1632 1633 self._rhodecode_user.user_id,
1633 1634 comment,
1634 1635 pull_request=pull_request
1635 1636 )
1636 1637
1637 1638 Session().flush()
1638 1639 # this is somehow required to get access to some relationship
1639 1640 # loaded on comment
1640 1641 Session().refresh(comment)
1641 1642
1642 1643 # skip notifications for drafts
1643 1644 if not is_draft:
1644 1645 PullRequestModel().trigger_pull_request_hook(
1645 1646 pull_request, self._rhodecode_user, 'comment',
1646 1647 data={'comment': comment})
1647 1648
1648 1649 # we now calculate the status of pull request, and based on that
1649 1650 # calculation we set the commits status
1650 1651 calculated_status = pull_request.calculated_review_status()
1651 1652 if old_calculated_status != calculated_status:
1652 1653 PullRequestModel().trigger_pull_request_hook(
1653 1654 pull_request, self._rhodecode_user, 'review_status_change',
1654 1655 data={'status': calculated_status})
1655 1656
1656 1657 comment_id = comment.comment_id
1657 1658 data[comment_id] = {
1658 1659 'target_id': target_elem_id
1659 1660 }
1660 1661 Session().flush()
1661 1662
1662 1663 c.co = comment
1663 1664 c.at_version_num = None
1664 1665 c.is_new = True
1665 1666 rendered_comment = render(
1666 1667 'rhodecode:templates/changeset/changeset_comment_block.mako',
1667 1668 self._get_template_context(c), self.request)
1668 1669
1669 1670 data[comment_id].update(comment.get_dict())
1670 1671 data[comment_id].update({'rendered_text': rendered_comment})
1671 1672
1672 1673 Session().commit()
1673 1674
1674 1675 # skip channelstream for draft comments
1675 1676 if not all_drafts:
1676 1677 comment_broadcast_channel = channelstream.comment_channel(
1677 1678 self.db_repo_name, pull_request_obj=pull_request)
1678 1679
1679 1680 comment_data = data
1680 1681 posted_comment_type = 'inline' if is_inline else 'general'
1681 1682 if len(data) == 1:
1682 1683 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1683 1684 else:
1684 1685 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1685 1686
1686 1687 channelstream.comment_channelstream_push(
1687 1688 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1688 1689 comment_data=comment_data)
1689 1690
1690 1691 return data
1691 1692
1692 1693 @LoginRequired()
1693 1694 @NotAnonymous()
1694 1695 @HasRepoPermissionAnyDecorator(
1695 1696 'repository.read', 'repository.write', 'repository.admin')
1696 1697 @CSRFRequired()
1697 1698 def pull_request_comment_create(self):
1698 1699 _ = self.request.translate
1699 1700
1700 1701 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1701 1702
1702 1703 if pull_request.is_closed():
1703 1704 log.debug('comment: forbidden because pull request is closed')
1704 1705 raise HTTPForbidden()
1705 1706
1706 1707 allowed_to_comment = PullRequestModel().check_user_comment(
1707 1708 pull_request, self._rhodecode_user)
1708 1709 if not allowed_to_comment:
1709 1710 log.debug('comment: forbidden because pull request is from forbidden repo')
1710 1711 raise HTTPForbidden()
1711 1712
1712 1713 comment_data = {
1713 1714 'comment_type': self.request.POST.get('comment_type'),
1714 1715 'text': self.request.POST.get('text'),
1715 1716 'status': self.request.POST.get('changeset_status', None),
1716 1717 'is_draft': self.request.POST.get('draft'),
1717 1718 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1718 1719 'close_pull_request': self.request.POST.get('close_pull_request'),
1719 1720 'f_path': self.request.POST.get('f_path'),
1720 1721 'line': self.request.POST.get('line'),
1721 1722 }
1722 1723 data = self._pull_request_comments_create(pull_request, [comment_data])
1723 1724
1724 1725 return data
1725 1726
1726 1727 @LoginRequired()
1727 1728 @NotAnonymous()
1728 1729 @HasRepoPermissionAnyDecorator(
1729 1730 'repository.read', 'repository.write', 'repository.admin')
1730 1731 @CSRFRequired()
1731 1732 def pull_request_comment_delete(self):
1732 1733 pull_request = PullRequest.get_or_404(
1733 1734 self.request.matchdict['pull_request_id'])
1734 1735
1735 1736 comment = ChangesetComment.get_or_404(
1736 1737 self.request.matchdict['comment_id'])
1737 1738 comment_id = comment.comment_id
1738 1739
1739 1740 if comment.immutable:
1740 1741 # don't allow deleting comments that are immutable
1741 1742 raise HTTPForbidden()
1742 1743
1743 1744 if pull_request.is_closed():
1744 1745 log.debug('comment: forbidden because pull request is closed')
1745 1746 raise HTTPForbidden()
1746 1747
1747 1748 if not comment:
1748 1749 log.debug('Comment with id:%s not found, skipping', comment_id)
1749 1750 # comment already deleted in another call probably
1750 1751 return True
1751 1752
1752 1753 if comment.pull_request.is_closed():
1753 1754 # don't allow deleting comments on closed pull request
1754 1755 raise HTTPForbidden()
1755 1756
1756 1757 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1757 1758 super_admin = h.HasPermissionAny('hg.admin')()
1758 1759 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1759 1760 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1760 1761 comment_repo_admin = is_repo_admin and is_repo_comment
1761 1762
1762 1763 if comment.draft and not comment_owner:
1763 1764 # We never allow to delete draft comments for other than owners
1764 1765 raise HTTPNotFound()
1765 1766
1766 1767 if super_admin or comment_owner or comment_repo_admin:
1767 1768 old_calculated_status = comment.pull_request.calculated_review_status()
1768 1769 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1769 1770 Session().commit()
1770 1771 calculated_status = comment.pull_request.calculated_review_status()
1771 1772 if old_calculated_status != calculated_status:
1772 1773 PullRequestModel().trigger_pull_request_hook(
1773 1774 comment.pull_request, self._rhodecode_user, 'review_status_change',
1774 1775 data={'status': calculated_status})
1775 1776 return True
1776 1777 else:
1777 1778 log.warning('No permissions for user %s to delete comment_id: %s',
1778 1779 self._rhodecode_db_user, comment_id)
1779 1780 raise HTTPNotFound()
1780 1781
1781 1782 @LoginRequired()
1782 1783 @NotAnonymous()
1783 1784 @HasRepoPermissionAnyDecorator(
1784 1785 'repository.read', 'repository.write', 'repository.admin')
1785 1786 @CSRFRequired()
1786 1787 def pull_request_comment_edit(self):
1787 1788 self.load_default_context()
1788 1789
1789 1790 pull_request = PullRequest.get_or_404(
1790 1791 self.request.matchdict['pull_request_id']
1791 1792 )
1792 1793 comment = ChangesetComment.get_or_404(
1793 1794 self.request.matchdict['comment_id']
1794 1795 )
1795 1796 comment_id = comment.comment_id
1796 1797
1797 1798 if comment.immutable:
1798 1799 # don't allow deleting comments that are immutable
1799 1800 raise HTTPForbidden()
1800 1801
1801 1802 if pull_request.is_closed():
1802 1803 log.debug('comment: forbidden because pull request is closed')
1803 1804 raise HTTPForbidden()
1804 1805
1805 1806 if comment.pull_request.is_closed():
1806 1807 # don't allow deleting comments on closed pull request
1807 1808 raise HTTPForbidden()
1808 1809
1809 1810 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1810 1811 super_admin = h.HasPermissionAny('hg.admin')()
1811 1812 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1812 1813 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1813 1814 comment_repo_admin = is_repo_admin and is_repo_comment
1814 1815
1815 1816 if super_admin or comment_owner or comment_repo_admin:
1816 1817 text = self.request.POST.get('text')
1817 1818 version = self.request.POST.get('version')
1818 1819 if text == comment.text:
1819 1820 log.warning(
1820 1821 'Comment(PR): '
1821 1822 'Trying to create new version '
1822 1823 'with the same comment body {}'.format(
1823 1824 comment_id,
1824 1825 )
1825 1826 )
1826 1827 raise HTTPNotFound()
1827 1828
1828 1829 if version.isdigit():
1829 1830 version = int(version)
1830 1831 else:
1831 1832 log.warning(
1832 1833 'Comment(PR): Wrong version type {} {} '
1833 1834 'for comment {}'.format(
1834 1835 version,
1835 1836 type(version),
1836 1837 comment_id,
1837 1838 )
1838 1839 )
1839 1840 raise HTTPNotFound()
1840 1841
1841 1842 try:
1842 1843 comment_history = CommentsModel().edit(
1843 1844 comment_id=comment_id,
1844 1845 text=text,
1845 1846 auth_user=self._rhodecode_user,
1846 1847 version=version,
1847 1848 )
1848 1849 except CommentVersionMismatch:
1849 1850 raise HTTPConflict()
1850 1851
1851 1852 if not comment_history:
1852 1853 raise HTTPNotFound()
1853 1854
1854 1855 Session().commit()
1855 1856 if not comment.draft:
1856 1857 PullRequestModel().trigger_pull_request_hook(
1857 1858 pull_request, self._rhodecode_user, 'comment_edit',
1858 1859 data={'comment': comment})
1859 1860
1860 1861 return {
1861 1862 'comment_history_id': comment_history.comment_history_id,
1862 1863 'comment_id': comment.comment_id,
1863 1864 'comment_version': comment_history.version,
1864 1865 'comment_author_username': comment_history.author.username,
1865 1866 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1866 1867 'comment_created_on': h.age_component(comment_history.created_on,
1867 1868 time_is_local=True),
1868 1869 }
1869 1870 else:
1870 1871 log.warning('No permissions for user %s to edit comment_id: %s',
1871 1872 self._rhodecode_db_user, comment_id)
1872 1873 raise HTTPNotFound()
@@ -1,1060 +1,1061 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 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5 5
6 6
7 7 <%def name="title()">
8 8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='showpullrequest')}
24 24 </%def>
25 25
26 26
27 27 <%def name="main()">
28 28 ## Container to gather extracted Tickets
29 29 <%
30 30 c.referenced_commit_issues = h.IssuesRegistry()
31 31 c.referenced_desc_issues = h.IssuesRegistry()
32 32 %>
33 33
34 34 <script type="text/javascript">
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
37 37 </script>
38 38
39 39 <div class="box">
40 40
41 41 <div class="box pr-summary">
42 42
43 43 <div class="summary-details block-left">
44 44 <div id="pr-title">
45 45 % if c.pull_request.is_closed():
46 46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
47 47 % endif
48 48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
49 49 </div>
50 50 <div id="pr-title-edit" class="input" style="display: none;">
51 51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
52 52 </div>
53 53
54 54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55 55 <div class="pr-details-title">
56 56 <div class="pull-left">
57 57 <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>
58 58 ${_('Created on')}
59 59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
60 60 <span class="pr-details-title-author-pref">${_('by')}</span>
61 61 </div>
62 62
63 63 <div class="pull-left">
64 64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
65 65 </div>
66 66
67 67 %if c.allowed_to_update:
68 68 <div class="pull-right">
69 69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
70 70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
71 71 % if c.allowed_to_delete:
72 72 ${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)}
73 73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
74 74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
75 75 type="submit" value="${_('Delete pull request')}">
76 76 ${h.end_form()}
77 77 % else:
78 78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
79 79 % endif
80 80 </div>
81 81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
82 82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
83 83 </div>
84 84
85 85 %endif
86 86 </div>
87 87
88 88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
89 89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container_callback=c.referenced_desc_issues())}
90 90 </div>
91 91
92 92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
93 93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
94 94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
95 95 </div>
96 96
97 97 <div id="summary" class="fields pr-details-content">
98 98
99 99 ## source
100 100 <div class="field">
101 101 <div class="label-pr-detail">
102 102 <label>${_('Commit flow')}:</label>
103 103 </div>
104 104 <div class="input">
105 105 <div class="pr-commit-flow">
106 106 ## Source
107 107 %if c.pull_request.source_ref_parts.type == 'branch':
108 108 <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>
109 109 %else:
110 110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
111 111 %endif
112 112 ${_('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>
113 113 &rarr;
114 114 ## Target
115 115 %if c.pull_request.target_ref_parts.type == 'branch':
116 116 <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>
117 117 %else:
118 118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
119 119 %endif
120 120
121 121 ${_('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>
122 122
123 123 <a class="source-details-action" href="#expand-source-details" onclick="return 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>'>
124 124 <i class="icon-angle-down">more details</i>
125 125 </a>
126 126
127 127 </div>
128 128
129 129 <div class="source-details" style="display: none">
130 130
131 131 <ul>
132 132
133 133 ## common ancestor
134 134 <li>
135 135 ${_('Common ancestor')}:
136 136 % if c.ancestor_commit:
137 137 <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>
138 138 % else:
139 139 ${_('not available')}
140 140 % endif
141 141 </li>
142 142
143 143 ## pull url
144 144 <li>
145 145 %if h.is_hg(c.pull_request.source_repo):
146 146 <% clone_url = u'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
147 147 %elif h.is_git(c.pull_request.source_repo):
148 148 <% clone_url = u'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
149 149 %endif
150 150
151 151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
152 152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
153 153 </li>
154 154
155 155 ## Shadow repo
156 156 <li>
157 157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
158 158 %if h.is_hg(c.pull_request.target_repo):
159 159 <% 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) %>
160 160 %elif h.is_git(c.pull_request.target_repo):
161 161 <% 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) %>
162 162 %endif
163 163
164 164 <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">
165 165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
166 166
167 167 % else:
168 168 <div class="">
169 169 ${_('Shadow repository data not available')}.
170 170 </div>
171 171 % endif
172 172 </li>
173 173
174 174 </ul>
175 175
176 176 </div>
177 177
178 178 </div>
179 179
180 180 </div>
181 181
182 182 ## versions
183 183 <div class="field">
184 184 <div class="label-pr-detail">
185 185 <label>${_('Versions')}:</label>
186 186 </div>
187 187
188 188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
189 189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
190 190
191 191 <div class="pr-versions">
192 192 % if c.show_version_changes:
193 193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
194 194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
195 195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
196 196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
197 197 data-toggle-on="${_('show versions')}."
198 198 data-toggle-off="${_('hide versions')}.">
199 199 ${_('show versions')}.
200 200 </a>
201 201 <table>
202 202 ## SHOW ALL VERSIONS OF PR
203 203 <% ver_pr = None %>
204 204
205 205 % for data in reversed(list(enumerate(c.versions, 1))):
206 206 <% ver_pos = data[0] %>
207 207 <% ver = data[1] %>
208 208 <% ver_pr = ver.pull_request_version_id %>
209 209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
210 210
211 211 <tr class="version-pr" style="display: ${display_row}">
212 212 <td>
213 213 <code>
214 214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
215 215 </code>
216 216 </td>
217 217 <td>
218 218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 219 <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}"/>
220 220 </td>
221 221 <td>
222 222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
223 223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224 224
225 225 </td>
226 226 <td>
227 227 % if c.at_version_num != ver_pr:
228 228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
229 229 <code>
230 230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
231 231 </code>
232 232 % endif
233 233 </td>
234 234 <td>
235 235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
236 236 </td>
237 237 <td>
238 238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
239 239 </td>
240 240 </tr>
241 241 % endfor
242 242
243 243 <tr>
244 244 <td colspan="6">
245 245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
246 246 data-label-text-locked="${_('select versions to show changes')}"
247 247 data-label-text-diff="${_('show changes between versions')}"
248 248 data-label-text-show="${_('show pull request for this version')}"
249 249 >
250 250 ${_('select versions to show changes')}
251 251 </button>
252 252 </td>
253 253 </tr>
254 254 </table>
255 255 % else:
256 256 <div>
257 257 ${_('Pull request versions not available')}.
258 258 </div>
259 259 % endif
260 260 </div>
261 261 </div>
262 262
263 263 </div>
264 264
265 265 </div>
266 266
267 267
268 268 </div>
269 269
270 270 </div>
271 271
272 272 <div class="box">
273 273
274 274 % if c.state_progressing:
275 275
276 276 <h2 style="text-align: center">
277 ${_('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>
277 ${_('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><br/>
278 ${_('Consider refreshing the page to check if the status transition was finished')}.
278 279
279 % if c.is_super_admin:
280 % if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name):
280 281 <br/>
281 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.
282 ${_('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.
282 283 % endif
283 284 </h2>
284 285
285 286 % else:
286 287
287 288 ## Diffs rendered here
288 289 <div class="table" >
289 290 <div id="changeset_compare_view_content">
290 291 ##CS
291 292 % if c.missing_requirements:
292 293 <div class="box">
293 294 <div class="alert alert-warning">
294 295 <div>
295 296 <strong>${_('Missing requirements:')}</strong>
296 297 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
297 298 </div>
298 299 </div>
299 300 </div>
300 301 % elif c.missing_commits:
301 302 <div class="box">
302 303 <div class="alert alert-warning">
303 304 <div>
304 305 <strong>${_('Missing commits')}:</strong>
305 306 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
306 307 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
307 308 ${_('Consider doing a `force update commits` in case you think this is an error.')}
308 309 </div>
309 310 </div>
310 311 </div>
311 312 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
312 313 <div class="box">
313 314 <div class="alert alert-info">
314 315 <div>
315 316 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
316 317 </div>
317 318 </div>
318 319 </div>
319 320 % endif
320 321
321 322 <div class="compare_view_commits_title">
322 323 % if not c.compare_mode:
323 324
324 325 % if c.at_version_index:
325 326 <h4>
326 327 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
327 328 </h4>
328 329 % endif
329 330
330 331 <div class="pull-left">
331 332 <div class="btn-group">
332 333 <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)} >
333 334 % if c.collapse_all_commits:
334 335 <i class="icon-plus-squared-alt icon-no-margin"></i>
335 336 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
336 337 % else:
337 338 <i class="icon-minus-squared-alt icon-no-margin"></i>
338 339 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
339 340 % endif
340 341 </a>
341 342 </div>
342 343 </div>
343 344
344 345 <div class="pull-right">
345 346 % if c.allowed_to_update and not c.pull_request.is_closed():
346 347
347 348 <div class="btn-group btn-group-actions">
348 349 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
349 350 ${_('Update commits')}
350 351 </a>
351 352
352 353 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
353 354 <i class="icon-down"></i>
354 355 </a>
355 356
356 357 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
357 358 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
358 359 <li>
359 360 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
360 361 ${_('Force update commits')}
361 362 </a>
362 363 <div class="action-help-block">
363 364 ${_('Update commits and force refresh this pull request.')}
364 365 </div>
365 366 </li>
366 367 </ul>
367 368 </div>
368 369 </div>
369 370
370 371 % else:
371 372 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
372 373 % endif
373 374
374 375 </div>
375 376 % endif
376 377 </div>
377 378
378 379 % if not c.missing_commits:
379 380 ## COMPARE RANGE DIFF MODE
380 381 % if c.compare_mode:
381 382 % if c.at_version:
382 383 <h4>
383 384 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
384 385 </h4>
385 386
386 387 <div class="subtitle-compare">
387 388 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
388 389 </div>
389 390
390 391 <div class="container">
391 392 <table class="rctable compare_view_commits">
392 393 <tr>
393 394 <th></th>
394 395 <th>${_('Time')}</th>
395 396 <th>${_('Author')}</th>
396 397 <th>${_('Commit')}</th>
397 398 <th></th>
398 399 <th>${_('Description')}</th>
399 400 </tr>
400 401
401 402 % for c_type, commit in c.commit_changes:
402 403 % if c_type in ['a', 'r']:
403 404 <%
404 405 if c_type == 'a':
405 406 cc_title = _('Commit added in displayed changes')
406 407 elif c_type == 'r':
407 408 cc_title = _('Commit removed in displayed changes')
408 409 else:
409 410 cc_title = ''
410 411 %>
411 412 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
412 413 <td>
413 414 <div class="commit-change-indicator color-${c_type}-border">
414 415 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
415 416 ${c_type.upper()}
416 417 </div>
417 418 </div>
418 419 </td>
419 420 <td class="td-time">
420 421 ${h.age_component(commit.date)}
421 422 </td>
422 423 <td class="td-user">
423 424 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
424 425 </td>
425 426 <td class="td-hash">
426 427 <code>
427 428 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
428 429 r${commit.idx}:${h.short_id(commit.raw_id)}
429 430 </a>
430 431 ${h.hidden('revisions', commit.raw_id)}
431 432 </code>
432 433 </td>
433 434 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
434 435 <i class="icon-expand-linked"></i>
435 436 </td>
436 437 <td class="mid td-description">
437 438 <div class="log-container truncate-wrap">
438 439 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container_callback=c.referenced_commit_issues(commit.serialize()))}</div>
439 440 </div>
440 441 </td>
441 442 </tr>
442 443 % endif
443 444 % endfor
444 445 </table>
445 446 </div>
446 447
447 448 % endif
448 449
449 450 ## Regular DIFF
450 451 % else:
451 452 <%include file="/compare/compare_commits.mako" />
452 453 % endif
453 454
454 455 <div class="cs_files">
455 456 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
456 457
457 458 <%
458 459 pr_menu_data = {
459 460 'outdated_comm_count_ver': outdated_comm_count_ver,
460 461 'pull_request': c.pull_request
461 462 }
462 463 %>
463 464
464 465 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
465 466
466 467 % if c.range_diff_on:
467 468 % for commit in c.commit_ranges:
468 469 ${cbdiffs.render_diffset(
469 470 c.changes[commit.raw_id],
470 471 commit=commit, use_comments=True,
471 472 collapse_when_files_over=5,
472 473 disable_new_comments=True,
473 474 deleted_files_comments=c.deleted_files_comments,
474 475 inline_comments=c.inline_comments,
475 476 pull_request_menu=pr_menu_data, show_todos=False)}
476 477 % endfor
477 478 % else:
478 479 ${cbdiffs.render_diffset(
479 480 c.diffset, use_comments=True,
480 481 collapse_when_files_over=30,
481 482 disable_new_comments=not c.allowed_to_comment,
482 483 deleted_files_comments=c.deleted_files_comments,
483 484 inline_comments=c.inline_comments,
484 485 pull_request_menu=pr_menu_data, show_todos=False)}
485 486 % endif
486 487
487 488 </div>
488 489 % else:
489 490 ## skipping commits we need to clear the view for missing commits
490 491 <div style="clear:both;"></div>
491 492 % endif
492 493
493 494 </div>
494 495 </div>
495 496
496 497 ## template for inline comment form
497 498 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
498 499
499 500 ## comments heading with count
500 501 <div class="comments-heading">
501 502 <i class="icon-comment"></i>
502 503 ${_('General Comments')} ${len(c.comments)}
503 504 </div>
504 505
505 506 ## render general comments
506 507 <div id="comment-tr-show">
507 508 % if general_outdated_comm_count_ver:
508 509 <div class="info-box">
509 510 % if general_outdated_comm_count_ver == 1:
510 511 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
511 512 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
512 513 % else:
513 514 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
514 515 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
515 516 % endif
516 517 </div>
517 518 % endif
518 519 </div>
519 520
520 521 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
521 522
522 523 % if not c.pull_request.is_closed():
523 524 ## main comment form and it status
524 525 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
525 526 pull_request_id=c.pull_request.pull_request_id),
526 527 c.pull_request_review_status,
527 528 is_pull_request=True, change_status=c.allowed_to_change_status)}
528 529
529 530 ## merge status, and merge action
530 531 <div class="pull-request-merge">
531 532 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
532 533 </div>
533 534
534 535 %endif
535 536
536 537 % endif
537 538 </div>
538 539
539 540
540 541 ### NAV SIDEBAR
541 542 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
542 543 <div class="sidenav navbar__inner" >
543 544 ## TOGGLE
544 545 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
545 546 <a href="#toggleSidebar" class="grey-link-action">
546 547
547 548 </a>
548 549 </div>
549 550
550 551 ## CONTENT
551 552 <div class="sidebar-content">
552 553
553 554 ## Drafts
554 555 % if c.rhodecode_edition_id == 'EE':
555 556 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 557 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 558 <i class="icon-comment icon-draft"></i>
558 559 <span id="drafts-count">${len(c.draft_comments)}</span>
559 560 </div>
560 561
561 562 <div class="right-sidebar-expanded-state pr-details-title">
562 563 <span style="padding-left: 2px">
563 564 <input name="select_all_drafts" type="checkbox" onclick="selectDraftComments(event)">
564 565 </span>
565 566 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
566 567 <i class="icon-comment icon-draft"></i>
567 568 ${_('Drafts')}
568 569 </span>
569 570 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
570 571 </div>
571 572
572 573 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
573 574 % if c.draft_comments:
574 575 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 576 % else:
576 577 <table class="drafts-content-table">
577 578 <tr>
578 579 <td>
579 580 ${_('No TODOs yet')}
580 581 </td>
581 582 </tr>
582 583 </table>
583 584 % endif
584 585 </div>
585 586
586 587 </div>
587 588 % endif
588 589
589 590 ## RULES SUMMARY/RULES
590 591 <div class="sidebar-element clear-both">
591 592 <% vote_title = _ungettext(
592 593 'Status calculated based on votes from {} reviewer',
593 594 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
594 595 %>
595 596
596 597 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
597 598 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
598 599 ${c.reviewers_count}
599 600 </div>
600 601
601 602 ## REVIEWERS
602 603 <div class="right-sidebar-expanded-state pr-details-title">
603 604 <span class="tooltip sidebar-heading" title="${vote_title}">
604 605 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
605 606 ${_('Reviewers')}
606 607 </span>
607 608
608 609 %if c.allowed_to_update:
609 610 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
610 611 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
611 612 %else:
612 613 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
613 614 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
614 615 %endif
615 616 </div>
616 617
617 618 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
618 619
619 620 <div id="review_rules" style="display: none" class="">
620 621
621 622 <strong>${_('Reviewer rules')}</strong>
622 623 <div class="pr-reviewer-rules">
623 624 ## review rules will be appended here, by default reviewers logic
624 625 </div>
625 626 <input id="review_data" type="hidden" name="review_data" value="">
626 627 </div>
627 628
628 629 ## members redering block
629 630 <input type="hidden" name="__start__" value="review_members:sequence">
630 631
631 632 <table id="review_members" class="group_members">
632 633 ## This content is loaded via JS and ReviewersPanel
633 634 </table>
634 635
635 636 <input type="hidden" name="__end__" value="review_members:sequence">
636 637 ## end members redering block
637 638
638 639 %if not c.pull_request.is_closed():
639 640 <div id="add_reviewer" class="ac" style="display: none;">
640 641 %if c.allowed_to_update:
641 642 % if not c.forbid_adding_reviewers:
642 643 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
643 644 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
644 645 <div id="reviewers_container"></div>
645 646 </div>
646 647 % endif
647 648 <div class="pull-right" style="margin-bottom: 15px">
648 649 <button data-role="reviewer" id="update_reviewers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
649 650 </div>
650 651 %endif
651 652 </div>
652 653 %endif
653 654 </div>
654 655 </div>
655 656
656 657 ## OBSERVERS
657 658 % if c.rhodecode_edition_id == 'EE':
658 659 <div class="sidebar-element clear-both">
659 660 <% vote_title = _ungettext(
660 661 '{} observer without voting right.',
661 662 '{} observers without voting right.', c.observers_count).format(c.observers_count)
662 663 %>
663 664
664 665 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
665 666 <i class="icon-circle-thin"></i>
666 667 ${c.observers_count}
667 668 </div>
668 669
669 670 <div class="right-sidebar-expanded-state pr-details-title">
670 671 <span class="tooltip sidebar-heading" title="${vote_title}">
671 672 <i class="icon-circle-thin"></i>
672 673 ${_('Observers')}
673 674 </span>
674 675 %if c.allowed_to_update:
675 676 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
676 677 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
677 678 %endif
678 679 </div>
679 680
680 681 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
681 682 ## members redering block
682 683 <input type="hidden" name="__start__" value="observer_members:sequence">
683 684
684 685 <table id="observer_members" class="group_members">
685 686 ## This content is loaded via JS and ReviewersPanel
686 687 </table>
687 688
688 689 <input type="hidden" name="__end__" value="observer_members:sequence">
689 690 ## end members redering block
690 691
691 692 %if not c.pull_request.is_closed():
692 693 <div id="add_observer" class="ac" style="display: none;">
693 694 %if c.allowed_to_update:
694 695 % if not c.forbid_adding_reviewers or 1:
695 696 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
696 697 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
697 698 <div id="observers_container"></div>
698 699 </div>
699 700 % endif
700 701 <div class="pull-right" style="margin-bottom: 15px">
701 702 <button data-role="observer" id="update_observers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
702 703 </div>
703 704 %endif
704 705 </div>
705 706 %endif
706 707 </div>
707 708 </div>
708 709 % endif
709 710
710 711 ## TODOs
711 712 <div id="todosTable" class="sidebar-element clear-both">
712 713 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
713 714 <i class="icon-flag-filled"></i>
714 715 <span id="todos-count">${len(c.unresolved_comments)}</span>
715 716 </div>
716 717
717 718 <div class="right-sidebar-expanded-state pr-details-title">
718 719 ## Only show unresolved, that is only what matters
719 720 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
720 721 <i class="icon-flag-filled"></i>
721 722 TODOs
722 723 </span>
723 724
724 725 % if not c.at_version:
725 726 % if c.resolved_comments:
726 727 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
727 728 % else:
728 729 <span class="block-right last-item noselect">Show resolved</span>
729 730 % endif
730 731 % endif
731 732 </div>
732 733
733 734 <div class="right-sidebar-expanded-state pr-details-content">
734 735
735 736 % if c.at_version:
736 737 <table>
737 738 <tr>
738 739 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
739 740 </tr>
740 741 </table>
741 742 % else:
742 743 % if c.unresolved_comments + c.resolved_comments:
743 744 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
744 745 % else:
745 746 <table class="todos-content-table">
746 747 <tr>
747 748 <td>
748 749 ${_('No TODOs yet')}
749 750 </td>
750 751 </tr>
751 752 </table>
752 753 % endif
753 754 % endif
754 755 </div>
755 756 </div>
756 757
757 758 ## COMMENTS
758 759 <div id="commentsTable" class="sidebar-element clear-both">
759 760 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
760 761 <i class="icon-comment" style="color: #949494"></i>
761 762 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
762 763 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
763 764 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
764 765 </div>
765 766
766 767 <div class="right-sidebar-expanded-state pr-details-title">
767 768 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
768 769 <i class="icon-comment" style="color: #949494"></i>
769 770 ${_('Comments')}
770 771
771 772 ## % if outdated_comm_count_ver:
772 773 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
773 774 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
774 775 ## </a>
775 776 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
776 777 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
777 778
778 779 ## % else:
779 780 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
780 781 ## % endif
781 782
782 783 </span>
783 784
784 785 % if outdated_comm_count_ver:
785 786 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
786 787 % else:
787 788 <span class="block-right last-item noselect">Show hidden</span>
788 789 % endif
789 790
790 791 </div>
791 792
792 793 <div class="right-sidebar-expanded-state pr-details-content">
793 794 % if c.inline_comments_flat + c.comments:
794 795 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
795 796 % else:
796 797 <table class="comments-content-table">
797 798 <tr>
798 799 <td>
799 800 ${_('No Comments yet')}
800 801 </td>
801 802 </tr>
802 803 </table>
803 804 % endif
804 805 </div>
805 806
806 807 </div>
807 808
808 809 ## Referenced Tickets
809 810 <div class="sidebar-element clear-both">
810 811 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
811 812 <i class="icon-info-circled"></i>
812 813 ${(c.referenced_desc_issues.issues_unique_count + c.referenced_commit_issues.issues_unique_count)}
813 814 </div>
814 815
815 816 <div class="right-sidebar-expanded-state pr-details-title">
816 817 <span class="sidebar-heading">
817 818 <i class="icon-info-circled"></i>
818 819 ${_('Referenced Tickets')}
819 820 </span>
820 821 </div>
821 822 <div class="right-sidebar-expanded-state pr-details-content">
822 823 <table>
823 824
824 825 <tr><td><code>${_('In pull request description')}:</code></td></tr>
825 826 % if c.referenced_desc_issues.issues:
826 827
827 828 % for ticket_id, ticket_dict in c.referenced_desc_issues.unique_issues.items():
828 829 <tr>
829 830 <td>
830 831 <a href="${ticket_dict[0].get('url')}">
831 832 ${ticket_id}
832 833 </a>
833 834 </td>
834 835 </tr>
835 836
836 837 % endfor
837 838 % else:
838 839 <tr>
839 840 <td>
840 841 ${_('No Ticket data found.')}
841 842 </td>
842 843 </tr>
843 844 % endif
844 845
845 846 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
846 847 % if c.referenced_commit_issues.issues:
847 848 % for ticket_id, ticket_dict in c.referenced_commit_issues.unique_issues.items():
848 849 <tr>
849 850 <td>
850 851 <a href="${ticket_dict[0].get('url')}">
851 852 ${ticket_id}
852 853 </a>
853 854 - ${_ungettext('in %s commit', 'in %s commits', len(ticket_dict)) % (len(ticket_dict))}
854 855 </td>
855 856 </tr>
856 857 % endfor
857 858 % else:
858 859 <tr>
859 860 <td>
860 861 ${_('No Ticket data found.')}
861 862 </td>
862 863 </tr>
863 864 % endif
864 865 </table>
865 866
866 867 </div>
867 868 </div>
868 869
869 870 </div>
870 871
871 872 </div>
872 873 </aside>
873 874
874 875 ## This JS needs to be at the end
875 876 <script type="text/javascript">
876 877
877 878 versionController = new VersionController();
878 879 versionController.init();
879 880
880 881 reviewersController = new ReviewersController();
881 882 commitsController = new CommitsController();
882 883 commentsController = new CommentsController();
883 884
884 885 updateController = new UpdatePrController();
885 886
886 887 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
887 888 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
888 889 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
889 890
890 891 (function () {
891 892 "use strict";
892 893
893 894 // custom code mirror
894 895 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
895 896
896 897 PRDetails.init();
897 898 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
898 899 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
899 900
900 901 window.showOutdated = function (self) {
901 902 $('.comment-inline.comment-outdated').show();
902 903 $('.filediff-outdated').show();
903 904 $('.showOutdatedComments').hide();
904 905 $('.hideOutdatedComments').show();
905 906 };
906 907
907 908 window.hideOutdated = function (self) {
908 909 $('.comment-inline.comment-outdated').hide();
909 910 $('.filediff-outdated').hide();
910 911 $('.hideOutdatedComments').hide();
911 912 $('.showOutdatedComments').show();
912 913 };
913 914
914 915 window.refreshMergeChecks = function () {
915 916 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
916 917 $('.pull-request-merge').css('opacity', 0.3);
917 918 $('.action-buttons-extra').css('opacity', 0.3);
918 919
919 920 $('.pull-request-merge').load(
920 921 loadUrl, function () {
921 922 $('.pull-request-merge').css('opacity', 1);
922 923
923 924 $('.action-buttons-extra').css('opacity', 1);
924 925 }
925 926 );
926 927 };
927 928
928 929 window.submitDrafts = function (event) {
929 930 var target = $(event.currentTarget);
930 931 var callback = function (result) {
931 932 target.removeAttr('onclick').html('saving...');
932 933 }
933 934 var draftIds = [];
934 935 $.each($('[name=submit_draft]:checked'), function (idx, val) {
935 936 draftIds.push(parseInt($(val).val()));
936 937 })
937 938 if (draftIds.length > 0) {
938 939 Rhodecode.comments.finalizeDrafts(draftIds, callback);
939 940 }
940 941 else {
941 942
942 943 }
943 944 }
944 945
945 946 window.selectDraftComments = function (event) {
946 947 var $target = $(event.currentTarget);
947 948 $('[name=submit_draft]').prop('checked', $target.prop('checked'))
948 949 }
949 950
950 951 window.closePullRequest = function (status) {
951 952 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
952 953 return false;
953 954 }
954 955 // inject closing flag
955 956 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
956 957 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
957 958 $(generalCommentForm.submitForm).submit();
958 959 };
959 960
960 961 //TODO this functionality is now missing
961 962 $('#show-outdated-comments').on('click', function (e) {
962 963 var button = $(this);
963 964 var outdated = $('.comment-outdated');
964 965
965 966 if (button.html() === "(Show)") {
966 967 button.html("(Hide)");
967 968 outdated.show();
968 969 } else {
969 970 button.html("(Show)");
970 971 outdated.hide();
971 972 }
972 973 });
973 974
974 975 $('#merge_pull_request_form').submit(function () {
975 976 if (!$('#merge_pull_request').attr('disabled')) {
976 977 $('#merge_pull_request').attr('disabled', 'disabled');
977 978 }
978 979 return true;
979 980 });
980 981
981 982 $('#edit_pull_request').on('click', function (e) {
982 983 var title = $('#pr-title-input').val();
983 984 var description = codeMirrorInstance.getValue();
984 985 var renderer = $('#pr-renderer-input').val();
985 986 editPullRequest(
986 987 "${c.repo_name}", "${c.pull_request.pull_request_id}",
987 988 title, description, renderer);
988 989 });
989 990
990 991 var $updateButtons = $('#update_reviewers,#update_observers');
991 992 $updateButtons.on('click', function (e) {
992 993 var role = $(this).data('role');
993 994 $updateButtons.attr('disabled', 'disabled');
994 995 $updateButtons.addClass('disabled');
995 996 $updateButtons.html(_gettext('Saving...'));
996 997 reviewersController.updateReviewers(
997 998 templateContext.repo_name,
998 999 templateContext.pull_request_data.pull_request_id,
999 1000 role
1000 1001 );
1001 1002 });
1002 1003
1003 1004 // fixing issue with caches on firefox
1004 1005 $('#update_commits').removeAttr("disabled");
1005 1006
1006 1007 $('.show-inline-comments').on('click', function (e) {
1007 1008 var boxid = $(this).attr('data-comment-id');
1008 1009 var button = $(this);
1009 1010
1010 1011 if (button.hasClass("comments-visible")) {
1011 1012 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1012 1013 $(this).hide();
1013 1014 });
1014 1015 button.removeClass("comments-visible");
1015 1016 } else {
1016 1017 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1017 1018 $(this).show();
1018 1019 });
1019 1020 button.addClass("comments-visible");
1020 1021 }
1021 1022 });
1022 1023
1023 1024 $('.show-inline-comments').on('change', function (e) {
1024 1025 var show = 'none';
1025 1026 var target = e.currentTarget;
1026 1027 if (target.checked) {
1027 1028 show = ''
1028 1029 }
1029 1030 var boxid = $(target).attr('id_for');
1030 1031 var comments = $('#{0} .inline-comments'.format(boxid));
1031 1032 var fn_display = function (idx) {
1032 1033 $(this).css('display', show);
1033 1034 };
1034 1035 $(comments).each(fn_display);
1035 1036 var btns = $('#{0} .inline-comments-button'.format(boxid));
1036 1037 $(btns).each(fn_display);
1037 1038 });
1038 1039
1039 1040 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1040 1041 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1041 1042 if (!comment.draft) {
1042 1043 refreshMergeChecks();
1043 1044 }
1044 1045 };
1045 1046
1046 1047 ReviewerAutoComplete('#user', reviewersController);
1047 1048 ObserverAutoComplete('#observer', reviewersController);
1048 1049
1049 1050 })();
1050 1051
1051 1052 $(document).ready(function () {
1052 1053
1053 1054 var channel = '${c.pr_broadcast_channel}';
1054 1055 new ReviewerPresenceController(channel)
1055 1056 // register globally so inject comment logic can re-use it.
1056 1057 window.commentsController = commentsController;
1057 1058 })
1058 1059 </script>
1059 1060
1060 1061 </%def>
General Comments 0
You need to be logged in to leave comments. Login now