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