##// END OF EJS Templates
comments[security]: make an additional check to forbid solving comments from other repo scope.
ergo -
r3546:4b3c3d76 default
parent child Browse files
Show More
@@ -1,690 +1,700 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from pyramid.threadlocal import get_current_registry, get_current_request
30 30 from sqlalchemy.sql.expression import null
31 31 from sqlalchemy.sql.functions import coalesce
32 32
33 33 from rhodecode.lib import helpers as h, diffs, channelstream
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 39 from rhodecode.model.notification import NotificationModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.model.notification import EmailNotificationModel
43 43 from rhodecode.model.validation_schema.schemas import comment_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CommentsModel(BaseModel):
50 50
51 51 cls = ChangesetComment
52 52
53 53 DIFF_CONTEXT_BEFORE = 3
54 54 DIFF_CONTEXT_AFTER = 3
55 55
56 56 def __get_commit_comment(self, changeset_comment):
57 57 return self._get_instance(ChangesetComment, changeset_comment)
58 58
59 59 def __get_pull_request(self, pull_request):
60 60 return self._get_instance(PullRequest, pull_request)
61 61
62 62 def _extract_mentions(self, s):
63 63 user_objects = []
64 64 for username in extract_mentioned_users(s):
65 65 user_obj = User.get_by_username(username, case_insensitive=True)
66 66 if user_obj:
67 67 user_objects.append(user_obj)
68 68 return user_objects
69 69
70 70 def _get_renderer(self, global_renderer='rst', request=None):
71 71 request = request or get_current_request()
72 72
73 73 try:
74 74 global_renderer = request.call_context.visual.default_renderer
75 75 except AttributeError:
76 76 log.debug("Renderer not set, falling back "
77 77 "to default renderer '%s'", global_renderer)
78 78 except Exception:
79 79 log.error(traceback.format_exc())
80 80 return global_renderer
81 81
82 82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 83 # group by versions, and count until, and display objects
84 84
85 85 comment_groups = collections.defaultdict(list)
86 86 [comment_groups[
87 87 _co.pull_request_version_id].append(_co) for _co in comments]
88 88
89 89 def yield_comments(pos):
90 90 for co in comment_groups[pos]:
91 91 yield co
92 92
93 93 comment_versions = collections.defaultdict(
94 94 lambda: collections.defaultdict(list))
95 95 prev_prvid = -1
96 96 # fake last entry with None, to aggregate on "latest" version which
97 97 # doesn't have an pull_request_version_id
98 98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 99 prvid = ver.pull_request_version_id
100 100 if prev_prvid == -1:
101 101 prev_prvid = prvid
102 102
103 103 for co in yield_comments(prvid):
104 104 comment_versions[prvid]['at'].append(co)
105 105
106 106 # save until
107 107 current = comment_versions[prvid]['at']
108 108 prev_until = comment_versions[prev_prvid]['until']
109 109 cur_until = prev_until + current
110 110 comment_versions[prvid]['until'].extend(cur_until)
111 111
112 112 # save outdated
113 113 if inline:
114 114 outdated = [x for x in cur_until
115 115 if x.outdated_at_version(show_version)]
116 116 else:
117 117 outdated = [x for x in cur_until
118 118 if x.older_than_version(show_version)]
119 119 display = [x for x in cur_until if x not in outdated]
120 120
121 121 comment_versions[prvid]['outdated'] = outdated
122 122 comment_versions[prvid]['display'] = display
123 123
124 124 prev_prvid = prvid
125 125
126 126 return comment_versions
127 127
128 128 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
129 129 qry = Session().query(ChangesetComment) \
130 130 .filter(ChangesetComment.repo == repo)
131 131
132 132 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
133 133 qry = qry.filter(ChangesetComment.comment_type == comment_type)
134 134
135 135 if user:
136 136 user = self._get_user(user)
137 137 if user:
138 138 qry = qry.filter(ChangesetComment.user_id == user.user_id)
139 139
140 140 if commit_id:
141 141 qry = qry.filter(ChangesetComment.revision == commit_id)
142 142
143 143 qry = qry.order_by(ChangesetComment.created_on)
144 144 return qry.all()
145 145
146 146 def get_repository_unresolved_todos(self, repo):
147 147 todos = Session().query(ChangesetComment) \
148 148 .filter(ChangesetComment.repo == repo) \
149 149 .filter(ChangesetComment.resolved_by == None) \
150 150 .filter(ChangesetComment.comment_type
151 151 == ChangesetComment.COMMENT_TYPE_TODO)
152 152 todos = todos.all()
153 153
154 154 return todos
155 155
156 156 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
157 157
158 158 todos = Session().query(ChangesetComment) \
159 159 .filter(ChangesetComment.pull_request == pull_request) \
160 160 .filter(ChangesetComment.resolved_by == None) \
161 161 .filter(ChangesetComment.comment_type
162 162 == ChangesetComment.COMMENT_TYPE_TODO)
163 163
164 164 if not show_outdated:
165 165 todos = todos.filter(
166 166 coalesce(ChangesetComment.display_state, '') !=
167 167 ChangesetComment.COMMENT_OUTDATED)
168 168
169 169 todos = todos.all()
170 170
171 171 return todos
172 172
173 173 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
174 174
175 175 todos = Session().query(ChangesetComment) \
176 176 .filter(ChangesetComment.revision == commit_id) \
177 177 .filter(ChangesetComment.resolved_by == None) \
178 178 .filter(ChangesetComment.comment_type
179 179 == ChangesetComment.COMMENT_TYPE_TODO)
180 180
181 181 if not show_outdated:
182 182 todos = todos.filter(
183 183 coalesce(ChangesetComment.display_state, '') !=
184 184 ChangesetComment.COMMENT_OUTDATED)
185 185
186 186 todos = todos.all()
187 187
188 188 return todos
189 189
190 190 def _log_audit_action(self, action, action_data, auth_user, comment):
191 191 audit_logger.store(
192 192 action=action,
193 193 action_data=action_data,
194 194 user=auth_user,
195 195 repo=comment.repo)
196 196
197 197 def create(self, text, repo, user, commit_id=None, pull_request=None,
198 198 f_path=None, line_no=None, status_change=None,
199 199 status_change_type=None, comment_type=None,
200 200 resolves_comment_id=None, closing_pr=False, send_email=True,
201 201 renderer=None, auth_user=None):
202 202 """
203 203 Creates new comment for commit or pull request.
204 204 IF status_change is not none this comment is associated with a
205 205 status change of commit or commit associated with pull request
206 206
207 207 :param text:
208 208 :param repo:
209 209 :param user:
210 210 :param commit_id:
211 211 :param pull_request:
212 212 :param f_path:
213 213 :param line_no:
214 214 :param status_change: Label for status change
215 215 :param comment_type: Type of comment
216 216 :param status_change_type: type of status change
217 217 :param closing_pr:
218 218 :param send_email:
219 219 :param renderer: pick renderer for this comment
220 220 """
221 221
222 222 if not text:
223 223 log.warning('Missing text for comment, skipping...')
224 224 return
225 225 request = get_current_request()
226 226 _ = request.translate
227 227
228 228 if not renderer:
229 229 renderer = self._get_renderer(request=request)
230 230
231 231 repo = self._get_repo(repo)
232 232 user = self._get_user(user)
233 233 auth_user = auth_user or user
234 234
235 235 schema = comment_schema.CommentSchema()
236 236 validated_kwargs = schema.deserialize(dict(
237 237 comment_body=text,
238 238 comment_type=comment_type,
239 239 comment_file=f_path,
240 240 comment_line=line_no,
241 241 renderer_type=renderer,
242 242 status_change=status_change_type,
243 243 resolves_comment_id=resolves_comment_id,
244 244 repo=repo.repo_id,
245 245 user=user.user_id,
246 246 ))
247 247
248 248 comment = ChangesetComment()
249 249 comment.renderer = validated_kwargs['renderer_type']
250 250 comment.text = validated_kwargs['comment_body']
251 251 comment.f_path = validated_kwargs['comment_file']
252 252 comment.line_no = validated_kwargs['comment_line']
253 253 comment.comment_type = validated_kwargs['comment_type']
254 254
255 255 comment.repo = repo
256 256 comment.author = user
257 257 resolved_comment = self.__get_commit_comment(
258 258 validated_kwargs['resolves_comment_id'])
259 259 # check if the comment actually belongs to this PR
260 260 if resolved_comment and resolved_comment.pull_request and \
261 261 resolved_comment.pull_request != pull_request:
262 log.warning('Comment tried to resolved unrelated todo comment: %s',
263 resolved_comment)
262 264 # comment not bound to this pull request, forbid
263 265 resolved_comment = None
266
267 elif resolved_comment and resolved_comment.repo and \
268 resolved_comment.repo != repo:
269 log.warning('Comment tried to resolved unrelated todo comment: %s',
270 resolved_comment)
271 # comment not bound to this repo, forbid
272 resolved_comment = None
273
264 274 comment.resolved_comment = resolved_comment
265 275
266 276 pull_request_id = pull_request
267 277
268 278 commit_obj = None
269 279 pull_request_obj = None
270 280
271 281 if commit_id:
272 282 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
273 283 # do a lookup, so we don't pass something bad here
274 284 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
275 285 comment.revision = commit_obj.raw_id
276 286
277 287 elif pull_request_id:
278 288 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
279 289 pull_request_obj = self.__get_pull_request(pull_request_id)
280 290 comment.pull_request = pull_request_obj
281 291 else:
282 292 raise Exception('Please specify commit or pull_request_id')
283 293
284 294 Session().add(comment)
285 295 Session().flush()
286 296 kwargs = {
287 297 'user': user,
288 298 'renderer_type': renderer,
289 299 'repo_name': repo.repo_name,
290 300 'status_change': status_change,
291 301 'status_change_type': status_change_type,
292 302 'comment_body': text,
293 303 'comment_file': f_path,
294 304 'comment_line': line_no,
295 305 'comment_type': comment_type or 'note'
296 306 }
297 307
298 308 if commit_obj:
299 309 recipients = ChangesetComment.get_users(
300 310 revision=commit_obj.raw_id)
301 311 # add commit author if it's in RhodeCode system
302 312 cs_author = User.get_from_cs_author(commit_obj.author)
303 313 if not cs_author:
304 314 # use repo owner if we cannot extract the author correctly
305 315 cs_author = repo.user
306 316 recipients += [cs_author]
307 317
308 318 commit_comment_url = self.get_url(comment, request=request)
309 319
310 320 target_repo_url = h.link_to(
311 321 repo.repo_name,
312 322 h.route_url('repo_summary', repo_name=repo.repo_name))
313 323
314 324 # commit specifics
315 325 kwargs.update({
316 326 'commit': commit_obj,
317 327 'commit_message': commit_obj.message,
318 328 'commit_target_repo': target_repo_url,
319 329 'commit_comment_url': commit_comment_url,
320 330 })
321 331
322 332 elif pull_request_obj:
323 333 # get the current participants of this pull request
324 334 recipients = ChangesetComment.get_users(
325 335 pull_request_id=pull_request_obj.pull_request_id)
326 336 # add pull request author
327 337 recipients += [pull_request_obj.author]
328 338
329 339 # add the reviewers to notification
330 340 recipients += [x.user for x in pull_request_obj.reviewers]
331 341
332 342 pr_target_repo = pull_request_obj.target_repo
333 343 pr_source_repo = pull_request_obj.source_repo
334 344
335 345 pr_comment_url = h.route_url(
336 346 'pullrequest_show',
337 347 repo_name=pr_target_repo.repo_name,
338 348 pull_request_id=pull_request_obj.pull_request_id,
339 349 _anchor='comment-%s' % comment.comment_id)
340 350
341 351 # set some variables for email notification
342 352 pr_target_repo_url = h.route_url(
343 353 'repo_summary', repo_name=pr_target_repo.repo_name)
344 354
345 355 pr_source_repo_url = h.route_url(
346 356 'repo_summary', repo_name=pr_source_repo.repo_name)
347 357
348 358 # pull request specifics
349 359 kwargs.update({
350 360 'pull_request': pull_request_obj,
351 361 'pr_id': pull_request_obj.pull_request_id,
352 362 'pr_target_repo': pr_target_repo,
353 363 'pr_target_repo_url': pr_target_repo_url,
354 364 'pr_source_repo': pr_source_repo,
355 365 'pr_source_repo_url': pr_source_repo_url,
356 366 'pr_comment_url': pr_comment_url,
357 367 'pr_closing': closing_pr,
358 368 })
359 369 if send_email:
360 370 # pre-generate the subject for notification itself
361 371 (subject,
362 372 _h, _e, # we don't care about those
363 373 body_plaintext) = EmailNotificationModel().render_email(
364 374 notification_type, **kwargs)
365 375
366 376 mention_recipients = set(
367 377 self._extract_mentions(text)).difference(recipients)
368 378
369 379 # create notification objects, and emails
370 380 NotificationModel().create(
371 381 created_by=user,
372 382 notification_subject=subject,
373 383 notification_body=body_plaintext,
374 384 notification_type=notification_type,
375 385 recipients=recipients,
376 386 mention_recipients=mention_recipients,
377 387 email_kwargs=kwargs,
378 388 )
379 389
380 390 Session().flush()
381 391 if comment.pull_request:
382 392 action = 'repo.pull_request.comment.create'
383 393 else:
384 394 action = 'repo.commit.comment.create'
385 395
386 396 comment_data = comment.get_api_data()
387 397 self._log_audit_action(
388 398 action, {'data': comment_data}, auth_user, comment)
389 399
390 400 msg_url = ''
391 401 channel = None
392 402 if commit_obj:
393 403 msg_url = commit_comment_url
394 404 repo_name = repo.repo_name
395 405 channel = u'/repo${}$/commit/{}'.format(
396 406 repo_name,
397 407 commit_obj.raw_id
398 408 )
399 409 elif pull_request_obj:
400 410 msg_url = pr_comment_url
401 411 repo_name = pr_target_repo.repo_name
402 412 channel = u'/repo${}$/pr/{}'.format(
403 413 repo_name,
404 414 pull_request_id
405 415 )
406 416
407 417 message = '<strong>{}</strong> {} - ' \
408 418 '<a onclick="window.location=\'{}\';' \
409 419 'window.location.reload()">' \
410 420 '<strong>{}</strong></a>'
411 421 message = message.format(
412 422 user.username, _('made a comment'), msg_url,
413 423 _('Show it now'))
414 424
415 425 channelstream.post_message(
416 426 channel, message, user.username,
417 427 registry=get_current_registry())
418 428
419 429 return comment
420 430
421 431 def delete(self, comment, auth_user):
422 432 """
423 433 Deletes given comment
424 434 """
425 435 comment = self.__get_commit_comment(comment)
426 436 old_data = comment.get_api_data()
427 437 Session().delete(comment)
428 438
429 439 if comment.pull_request:
430 440 action = 'repo.pull_request.comment.delete'
431 441 else:
432 442 action = 'repo.commit.comment.delete'
433 443
434 444 self._log_audit_action(
435 445 action, {'old_data': old_data}, auth_user, comment)
436 446
437 447 return comment
438 448
439 449 def get_all_comments(self, repo_id, revision=None, pull_request=None):
440 450 q = ChangesetComment.query()\
441 451 .filter(ChangesetComment.repo_id == repo_id)
442 452 if revision:
443 453 q = q.filter(ChangesetComment.revision == revision)
444 454 elif pull_request:
445 455 pull_request = self.__get_pull_request(pull_request)
446 456 q = q.filter(ChangesetComment.pull_request == pull_request)
447 457 else:
448 458 raise Exception('Please specify commit or pull_request')
449 459 q = q.order_by(ChangesetComment.created_on)
450 460 return q.all()
451 461
452 462 def get_url(self, comment, request=None, permalink=False):
453 463 if not request:
454 464 request = get_current_request()
455 465
456 466 comment = self.__get_commit_comment(comment)
457 467 if comment.pull_request:
458 468 pull_request = comment.pull_request
459 469 if permalink:
460 470 return request.route_url(
461 471 'pull_requests_global',
462 472 pull_request_id=pull_request.pull_request_id,
463 473 _anchor='comment-%s' % comment.comment_id)
464 474 else:
465 475 return request.route_url(
466 476 'pullrequest_show',
467 477 repo_name=safe_str(pull_request.target_repo.repo_name),
468 478 pull_request_id=pull_request.pull_request_id,
469 479 _anchor='comment-%s' % comment.comment_id)
470 480
471 481 else:
472 482 repo = comment.repo
473 483 commit_id = comment.revision
474 484
475 485 if permalink:
476 486 return request.route_url(
477 487 'repo_commit', repo_name=safe_str(repo.repo_id),
478 488 commit_id=commit_id,
479 489 _anchor='comment-%s' % comment.comment_id)
480 490
481 491 else:
482 492 return request.route_url(
483 493 'repo_commit', repo_name=safe_str(repo.repo_name),
484 494 commit_id=commit_id,
485 495 _anchor='comment-%s' % comment.comment_id)
486 496
487 497 def get_comments(self, repo_id, revision=None, pull_request=None):
488 498 """
489 499 Gets main comments based on revision or pull_request_id
490 500
491 501 :param repo_id:
492 502 :param revision:
493 503 :param pull_request:
494 504 """
495 505
496 506 q = ChangesetComment.query()\
497 507 .filter(ChangesetComment.repo_id == repo_id)\
498 508 .filter(ChangesetComment.line_no == None)\
499 509 .filter(ChangesetComment.f_path == None)
500 510 if revision:
501 511 q = q.filter(ChangesetComment.revision == revision)
502 512 elif pull_request:
503 513 pull_request = self.__get_pull_request(pull_request)
504 514 q = q.filter(ChangesetComment.pull_request == pull_request)
505 515 else:
506 516 raise Exception('Please specify commit or pull_request')
507 517 q = q.order_by(ChangesetComment.created_on)
508 518 return q.all()
509 519
510 520 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
511 521 q = self._get_inline_comments_query(repo_id, revision, pull_request)
512 522 return self._group_comments_by_path_and_line_number(q)
513 523
514 524 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
515 525 version=None):
516 526 inline_cnt = 0
517 527 for fname, per_line_comments in inline_comments.iteritems():
518 528 for lno, comments in per_line_comments.iteritems():
519 529 for comm in comments:
520 530 if not comm.outdated_at_version(version) and skip_outdated:
521 531 inline_cnt += 1
522 532
523 533 return inline_cnt
524 534
525 535 def get_outdated_comments(self, repo_id, pull_request):
526 536 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
527 537 # of a pull request.
528 538 q = self._all_inline_comments_of_pull_request(pull_request)
529 539 q = q.filter(
530 540 ChangesetComment.display_state ==
531 541 ChangesetComment.COMMENT_OUTDATED
532 542 ).order_by(ChangesetComment.comment_id.asc())
533 543
534 544 return self._group_comments_by_path_and_line_number(q)
535 545
536 546 def _get_inline_comments_query(self, repo_id, revision, pull_request):
537 547 # TODO: johbo: Split this into two methods: One for PR and one for
538 548 # commit.
539 549 if revision:
540 550 q = Session().query(ChangesetComment).filter(
541 551 ChangesetComment.repo_id == repo_id,
542 552 ChangesetComment.line_no != null(),
543 553 ChangesetComment.f_path != null(),
544 554 ChangesetComment.revision == revision)
545 555
546 556 elif pull_request:
547 557 pull_request = self.__get_pull_request(pull_request)
548 558 if not CommentsModel.use_outdated_comments(pull_request):
549 559 q = self._visible_inline_comments_of_pull_request(pull_request)
550 560 else:
551 561 q = self._all_inline_comments_of_pull_request(pull_request)
552 562
553 563 else:
554 564 raise Exception('Please specify commit or pull_request_id')
555 565 q = q.order_by(ChangesetComment.comment_id.asc())
556 566 return q
557 567
558 568 def _group_comments_by_path_and_line_number(self, q):
559 569 comments = q.all()
560 570 paths = collections.defaultdict(lambda: collections.defaultdict(list))
561 571 for co in comments:
562 572 paths[co.f_path][co.line_no].append(co)
563 573 return paths
564 574
565 575 @classmethod
566 576 def needed_extra_diff_context(cls):
567 577 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
568 578
569 579 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
570 580 if not CommentsModel.use_outdated_comments(pull_request):
571 581 return
572 582
573 583 comments = self._visible_inline_comments_of_pull_request(pull_request)
574 584 comments_to_outdate = comments.all()
575 585
576 586 for comment in comments_to_outdate:
577 587 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
578 588
579 589 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
580 590 diff_line = _parse_comment_line_number(comment.line_no)
581 591
582 592 try:
583 593 old_context = old_diff_proc.get_context_of_line(
584 594 path=comment.f_path, diff_line=diff_line)
585 595 new_context = new_diff_proc.get_context_of_line(
586 596 path=comment.f_path, diff_line=diff_line)
587 597 except (diffs.LineNotInDiffException,
588 598 diffs.FileNotInDiffException):
589 599 comment.display_state = ChangesetComment.COMMENT_OUTDATED
590 600 return
591 601
592 602 if old_context == new_context:
593 603 return
594 604
595 605 if self._should_relocate_diff_line(diff_line):
596 606 new_diff_lines = new_diff_proc.find_context(
597 607 path=comment.f_path, context=old_context,
598 608 offset=self.DIFF_CONTEXT_BEFORE)
599 609 if not new_diff_lines:
600 610 comment.display_state = ChangesetComment.COMMENT_OUTDATED
601 611 else:
602 612 new_diff_line = self._choose_closest_diff_line(
603 613 diff_line, new_diff_lines)
604 614 comment.line_no = _diff_to_comment_line_number(new_diff_line)
605 615 else:
606 616 comment.display_state = ChangesetComment.COMMENT_OUTDATED
607 617
608 618 def _should_relocate_diff_line(self, diff_line):
609 619 """
610 620 Checks if relocation shall be tried for the given `diff_line`.
611 621
612 622 If a comment points into the first lines, then we can have a situation
613 623 that after an update another line has been added on top. In this case
614 624 we would find the context still and move the comment around. This
615 625 would be wrong.
616 626 """
617 627 should_relocate = (
618 628 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
619 629 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
620 630 return should_relocate
621 631
622 632 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
623 633 candidate = new_diff_lines[0]
624 634 best_delta = _diff_line_delta(diff_line, candidate)
625 635 for new_diff_line in new_diff_lines[1:]:
626 636 delta = _diff_line_delta(diff_line, new_diff_line)
627 637 if delta < best_delta:
628 638 candidate = new_diff_line
629 639 best_delta = delta
630 640 return candidate
631 641
632 642 def _visible_inline_comments_of_pull_request(self, pull_request):
633 643 comments = self._all_inline_comments_of_pull_request(pull_request)
634 644 comments = comments.filter(
635 645 coalesce(ChangesetComment.display_state, '') !=
636 646 ChangesetComment.COMMENT_OUTDATED)
637 647 return comments
638 648
639 649 def _all_inline_comments_of_pull_request(self, pull_request):
640 650 comments = Session().query(ChangesetComment)\
641 651 .filter(ChangesetComment.line_no != None)\
642 652 .filter(ChangesetComment.f_path != None)\
643 653 .filter(ChangesetComment.pull_request == pull_request)
644 654 return comments
645 655
646 656 def _all_general_comments_of_pull_request(self, pull_request):
647 657 comments = Session().query(ChangesetComment)\
648 658 .filter(ChangesetComment.line_no == None)\
649 659 .filter(ChangesetComment.f_path == None)\
650 660 .filter(ChangesetComment.pull_request == pull_request)
651 661 return comments
652 662
653 663 @staticmethod
654 664 def use_outdated_comments(pull_request):
655 665 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
656 666 settings = settings_model.get_general_settings()
657 667 return settings.get('rhodecode_use_outdated_comments', False)
658 668
659 669
660 670 def _parse_comment_line_number(line_no):
661 671 """
662 672 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
663 673 """
664 674 old_line = None
665 675 new_line = None
666 676 if line_no.startswith('o'):
667 677 old_line = int(line_no[1:])
668 678 elif line_no.startswith('n'):
669 679 new_line = int(line_no[1:])
670 680 else:
671 681 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
672 682 return diffs.DiffLineNumber(old_line, new_line)
673 683
674 684
675 685 def _diff_to_comment_line_number(diff_line):
676 686 if diff_line.new is not None:
677 687 return u'n{}'.format(diff_line.new)
678 688 elif diff_line.old is not None:
679 689 return u'o{}'.format(diff_line.old)
680 690 return u''
681 691
682 692
683 693 def _diff_line_delta(a, b):
684 694 if None not in (a.new, b.new):
685 695 return abs(a.new - b.new)
686 696 elif None not in (a.old, b.old):
687 697 return abs(a.old - b.old)
688 698 else:
689 699 raise ValueError(
690 700 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now