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