##// END OF EJS Templates
fix(pull-requests): fixes for rendering comments
super-admin -
r5211:5e903185 default
parent child Browse files
Show More

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

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