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