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