##// END OF EJS Templates
channelstream: don't try to communicate with ws server if not explictly enabled
ergo -
r546:c224f1f2 default
parent child Browse files
Show More
@@ -1,513 +1,512 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 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)
42 ChangesetComment, User, Notification, PullRequest)
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
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class ChangesetCommentsModel(BaseModel):
51 class ChangesetCommentsModel(BaseModel):
52
52
53 cls = ChangesetComment
53 cls = ChangesetComment
54
54
55 DIFF_CONTEXT_BEFORE = 3
55 DIFF_CONTEXT_BEFORE = 3
56 DIFF_CONTEXT_AFTER = 3
56 DIFF_CONTEXT_AFTER = 3
57
57
58 def __get_commit_comment(self, changeset_comment):
58 def __get_commit_comment(self, changeset_comment):
59 return self._get_instance(ChangesetComment, changeset_comment)
59 return self._get_instance(ChangesetComment, changeset_comment)
60
60
61 def __get_pull_request(self, pull_request):
61 def __get_pull_request(self, pull_request):
62 return self._get_instance(PullRequest, pull_request)
62 return self._get_instance(PullRequest, pull_request)
63
63
64 def _extract_mentions(self, s):
64 def _extract_mentions(self, s):
65 user_objects = []
65 user_objects = []
66 for username in extract_mentioned_users(s):
66 for username in extract_mentioned_users(s):
67 user_obj = User.get_by_username(username, case_insensitive=True)
67 user_obj = User.get_by_username(username, case_insensitive=True)
68 if user_obj:
68 if user_obj:
69 user_objects.append(user_obj)
69 user_objects.append(user_obj)
70 return user_objects
70 return user_objects
71
71
72 def _get_renderer(self, global_renderer='rst'):
72 def _get_renderer(self, global_renderer='rst'):
73 try:
73 try:
74 # try reading from visual context
74 # try reading from visual context
75 from pylons import tmpl_context
75 from pylons import tmpl_context
76 global_renderer = tmpl_context.visual.default_renderer
76 global_renderer = tmpl_context.visual.default_renderer
77 except AttributeError:
77 except AttributeError:
78 log.debug("Renderer not set, falling back "
78 log.debug("Renderer not set, falling back "
79 "to default renderer '%s'", global_renderer)
79 "to default renderer '%s'", global_renderer)
80 except Exception:
80 except Exception:
81 log.error(traceback.format_exc())
81 log.error(traceback.format_exc())
82 return global_renderer
82 return global_renderer
83
83
84 def create(self, text, repo, user, revision=None, pull_request=None,
84 def create(self, text, repo, user, revision=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None, closing_pr=False,
85 f_path=None, line_no=None, status_change=None, closing_pr=False,
86 send_email=True, renderer=None):
86 send_email=True, renderer=None):
87 """
87 """
88 Creates new comment for commit or pull request.
88 Creates new comment for commit or pull request.
89 IF status_change is not none this comment is associated with a
89 IF status_change is not none this comment is associated with a
90 status change of commit or commit associated with pull request
90 status change of commit or commit associated with pull request
91
91
92 :param text:
92 :param text:
93 :param repo:
93 :param repo:
94 :param user:
94 :param user:
95 :param revision:
95 :param revision:
96 :param pull_request:
96 :param pull_request:
97 :param f_path:
97 :param f_path:
98 :param line_no:
98 :param line_no:
99 :param status_change:
99 :param status_change:
100 :param closing_pr:
100 :param closing_pr:
101 :param send_email:
101 :param send_email:
102 """
102 """
103 if not text:
103 if not text:
104 log.warning('Missing text for comment, skipping...')
104 log.warning('Missing text for comment, skipping...')
105 return
105 return
106
106
107 if not renderer:
107 if not renderer:
108 renderer = self._get_renderer()
108 renderer = self._get_renderer()
109
109
110 repo = self._get_repo(repo)
110 repo = self._get_repo(repo)
111 user = self._get_user(user)
111 user = self._get_user(user)
112 comment = ChangesetComment()
112 comment = ChangesetComment()
113 comment.renderer = renderer
113 comment.renderer = renderer
114 comment.repo = repo
114 comment.repo = repo
115 comment.author = user
115 comment.author = user
116 comment.text = text
116 comment.text = text
117 comment.f_path = f_path
117 comment.f_path = f_path
118 comment.line_no = line_no
118 comment.line_no = line_no
119
119
120 #TODO (marcink): fix this and remove revision as param
120 #TODO (marcink): fix this and remove revision as param
121 commit_id = revision
121 commit_id = revision
122 pull_request_id = pull_request
122 pull_request_id = pull_request
123
123
124 commit_obj = None
124 commit_obj = None
125 pull_request_obj = None
125 pull_request_obj = None
126
126
127 if commit_id:
127 if commit_id:
128 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
128 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
129 # do a lookup, so we don't pass something bad here
129 # do a lookup, so we don't pass something bad here
130 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
130 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
131 comment.revision = commit_obj.raw_id
131 comment.revision = commit_obj.raw_id
132
132
133 elif pull_request_id:
133 elif pull_request_id:
134 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
134 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
135 pull_request_obj = self.__get_pull_request(pull_request_id)
135 pull_request_obj = self.__get_pull_request(pull_request_id)
136 comment.pull_request = pull_request_obj
136 comment.pull_request = pull_request_obj
137 else:
137 else:
138 raise Exception('Please specify commit or pull_request_id')
138 raise Exception('Please specify commit or pull_request_id')
139
139
140 Session().add(comment)
140 Session().add(comment)
141 Session().flush()
141 Session().flush()
142 kwargs = {
142 kwargs = {
143 'user': user,
143 'user': user,
144 'renderer_type': renderer,
144 'renderer_type': renderer,
145 'repo_name': repo.repo_name,
145 'repo_name': repo.repo_name,
146 'status_change': status_change,
146 'status_change': status_change,
147 'comment_body': text,
147 'comment_body': text,
148 'comment_file': f_path,
148 'comment_file': f_path,
149 'comment_line': line_no,
149 'comment_line': line_no,
150 }
150 }
151
151
152 if commit_obj:
152 if commit_obj:
153 recipients = ChangesetComment.get_users(
153 recipients = ChangesetComment.get_users(
154 revision=commit_obj.raw_id)
154 revision=commit_obj.raw_id)
155 # add commit author if it's in RhodeCode system
155 # add commit author if it's in RhodeCode system
156 cs_author = User.get_from_cs_author(commit_obj.author)
156 cs_author = User.get_from_cs_author(commit_obj.author)
157 if not cs_author:
157 if not cs_author:
158 # use repo owner if we cannot extract the author correctly
158 # use repo owner if we cannot extract the author correctly
159 cs_author = repo.user
159 cs_author = repo.user
160 recipients += [cs_author]
160 recipients += [cs_author]
161
161
162 commit_comment_url = self.get_url(comment)
162 commit_comment_url = self.get_url(comment)
163
163
164 target_repo_url = h.link_to(
164 target_repo_url = h.link_to(
165 repo.repo_name,
165 repo.repo_name,
166 h.url('summary_home',
166 h.url('summary_home',
167 repo_name=repo.repo_name, qualified=True))
167 repo_name=repo.repo_name, qualified=True))
168
168
169 # commit specifics
169 # commit specifics
170 kwargs.update({
170 kwargs.update({
171 'commit': commit_obj,
171 'commit': commit_obj,
172 'commit_message': commit_obj.message,
172 'commit_message': commit_obj.message,
173 'commit_target_repo': target_repo_url,
173 'commit_target_repo': target_repo_url,
174 'commit_comment_url': commit_comment_url,
174 'commit_comment_url': commit_comment_url,
175 })
175 })
176
176
177 elif pull_request_obj:
177 elif pull_request_obj:
178 # get the current participants of this pull request
178 # get the current participants of this pull request
179 recipients = ChangesetComment.get_users(
179 recipients = ChangesetComment.get_users(
180 pull_request_id=pull_request_obj.pull_request_id)
180 pull_request_id=pull_request_obj.pull_request_id)
181 # add pull request author
181 # add pull request author
182 recipients += [pull_request_obj.author]
182 recipients += [pull_request_obj.author]
183
183
184 # add the reviewers to notification
184 # add the reviewers to notification
185 recipients += [x.user for x in pull_request_obj.reviewers]
185 recipients += [x.user for x in pull_request_obj.reviewers]
186
186
187 pr_target_repo = pull_request_obj.target_repo
187 pr_target_repo = pull_request_obj.target_repo
188 pr_source_repo = pull_request_obj.source_repo
188 pr_source_repo = pull_request_obj.source_repo
189
189
190 pr_comment_url = h.url(
190 pr_comment_url = h.url(
191 'pullrequest_show',
191 'pullrequest_show',
192 repo_name=pr_target_repo.repo_name,
192 repo_name=pr_target_repo.repo_name,
193 pull_request_id=pull_request_obj.pull_request_id,
193 pull_request_id=pull_request_obj.pull_request_id,
194 anchor='comment-%s' % comment.comment_id,
194 anchor='comment-%s' % comment.comment_id,
195 qualified=True,)
195 qualified=True,)
196
196
197 # set some variables for email notification
197 # set some variables for email notification
198 pr_target_repo_url = h.url(
198 pr_target_repo_url = h.url(
199 'summary_home', repo_name=pr_target_repo.repo_name,
199 'summary_home', repo_name=pr_target_repo.repo_name,
200 qualified=True)
200 qualified=True)
201
201
202 pr_source_repo_url = h.url(
202 pr_source_repo_url = h.url(
203 'summary_home', repo_name=pr_source_repo.repo_name,
203 'summary_home', repo_name=pr_source_repo.repo_name,
204 qualified=True)
204 qualified=True)
205
205
206 # pull request specifics
206 # pull request specifics
207 kwargs.update({
207 kwargs.update({
208 'pull_request': pull_request_obj,
208 'pull_request': pull_request_obj,
209 'pr_id': pull_request_obj.pull_request_id,
209 'pr_id': pull_request_obj.pull_request_id,
210 'pr_target_repo': pr_target_repo,
210 'pr_target_repo': pr_target_repo,
211 'pr_target_repo_url': pr_target_repo_url,
211 'pr_target_repo_url': pr_target_repo_url,
212 'pr_source_repo': pr_source_repo,
212 'pr_source_repo': pr_source_repo,
213 'pr_source_repo_url': pr_source_repo_url,
213 'pr_source_repo_url': pr_source_repo_url,
214 'pr_comment_url': pr_comment_url,
214 'pr_comment_url': pr_comment_url,
215 'pr_closing': closing_pr,
215 'pr_closing': closing_pr,
216 })
216 })
217 if send_email:
217 if send_email:
218 # pre-generate the subject for notification itself
218 # pre-generate the subject for notification itself
219 (subject,
219 (subject,
220 _h, _e, # we don't care about those
220 _h, _e, # we don't care about those
221 body_plaintext) = EmailNotificationModel().render_email(
221 body_plaintext) = EmailNotificationModel().render_email(
222 notification_type, **kwargs)
222 notification_type, **kwargs)
223
223
224 mention_recipients = set(
224 mention_recipients = set(
225 self._extract_mentions(text)).difference(recipients)
225 self._extract_mentions(text)).difference(recipients)
226
226
227 # create notification objects, and emails
227 # create notification objects, and emails
228 NotificationModel().create(
228 NotificationModel().create(
229 created_by=user,
229 created_by=user,
230 notification_subject=subject,
230 notification_subject=subject,
231 notification_body=body_plaintext,
231 notification_body=body_plaintext,
232 notification_type=notification_type,
232 notification_type=notification_type,
233 recipients=recipients,
233 recipients=recipients,
234 mention_recipients=mention_recipients,
234 mention_recipients=mention_recipients,
235 email_kwargs=kwargs,
235 email_kwargs=kwargs,
236 )
236 )
237
237
238 action = (
238 action = (
239 'user_commented_pull_request:{}'.format(
239 'user_commented_pull_request:{}'.format(
240 comment.pull_request.pull_request_id)
240 comment.pull_request.pull_request_id)
241 if comment.pull_request
241 if comment.pull_request
242 else 'user_commented_revision:{}'.format(comment.revision)
242 else 'user_commented_revision:{}'.format(comment.revision)
243 )
243 )
244 action_logger(user, action, comment.repo)
244 action_logger(user, action, comment.repo)
245
245
246 registry = get_current_registry()
246 registry = get_current_registry()
247 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
247 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
248 channelstream_config = rhodecode_plugins.get('channelstream')
248 channelstream_config = rhodecode_plugins.get('channelstream', {})
249 msg_url = ''
249 msg_url = ''
250 if commit_obj:
250 if commit_obj:
251 msg_url = commit_comment_url
251 msg_url = commit_comment_url
252 repo_name = repo.repo_name
252 repo_name = repo.repo_name
253 elif pull_request_obj:
253 elif pull_request_obj:
254 msg_url = pr_comment_url
254 msg_url = pr_comment_url
255 repo_name = pr_target_repo.repo_name
255 repo_name = pr_target_repo.repo_name
256
256
257 if channelstream_config:
257 if channelstream_config.get('enabled'):
258 message = '<strong>{}</strong> {} - ' \
258 message = '<strong>{}</strong> {} - ' \
259 '<a onclick="window.location=\'{}\';' \
259 '<a onclick="window.location=\'{}\';' \
260 'window.location.reload()">' \
260 'window.location.reload()">' \
261 '<strong>{}</strong></a>'
261 '<strong>{}</strong></a>'
262 message = message.format(
262 message = message.format(
263 user.username, _('made a comment'), msg_url,
263 user.username, _('made a comment'), msg_url,
264 _('Refresh page'))
264 _('Refresh page'))
265 if channelstream_config:
265 channel = '/repo${}$/pr/{}'.format(
266 channel = '/repo${}$/pr/{}'.format(
266 repo_name,
267 repo_name,
267 pull_request_id
268 pull_request_id
268 )
269 )
269 payload = {
270 payload = {
270 'type': 'message',
271 'type': 'message',
271 'timestamp': datetime.utcnow(),
272 'timestamp': datetime.utcnow(),
272 'user': 'system',
273 'user': 'system',
273 'exclude_users': [user.username],
274 'exclude_users': [user.username],
274 'channel': channel,
275 'channel': channel,
275 'message': {
276 'message': {
276 'message': message,
277 'message': message,
277 'level': 'info',
278 'level': 'info',
278 'topic': '/notifications'
279 'topic': '/notifications'
280 }
281 }
279 }
282 channelstream_request(channelstream_config, [payload],
280 }
283 '/message', raise_exc=False)
281 channelstream_request(channelstream_config, [payload],
282 '/message', raise_exc=False)
284
283
285 return comment
284 return comment
286
285
287 def delete(self, comment):
286 def delete(self, comment):
288 """
287 """
289 Deletes given comment
288 Deletes given comment
290
289
291 :param comment_id:
290 :param comment_id:
292 """
291 """
293 comment = self.__get_commit_comment(comment)
292 comment = self.__get_commit_comment(comment)
294 Session().delete(comment)
293 Session().delete(comment)
295
294
296 return comment
295 return comment
297
296
298 def get_all_comments(self, repo_id, revision=None, pull_request=None):
297 def get_all_comments(self, repo_id, revision=None, pull_request=None):
299 q = ChangesetComment.query()\
298 q = ChangesetComment.query()\
300 .filter(ChangesetComment.repo_id == repo_id)
299 .filter(ChangesetComment.repo_id == repo_id)
301 if revision:
300 if revision:
302 q = q.filter(ChangesetComment.revision == revision)
301 q = q.filter(ChangesetComment.revision == revision)
303 elif pull_request:
302 elif pull_request:
304 pull_request = self.__get_pull_request(pull_request)
303 pull_request = self.__get_pull_request(pull_request)
305 q = q.filter(ChangesetComment.pull_request == pull_request)
304 q = q.filter(ChangesetComment.pull_request == pull_request)
306 else:
305 else:
307 raise Exception('Please specify commit or pull_request')
306 raise Exception('Please specify commit or pull_request')
308 q = q.order_by(ChangesetComment.created_on)
307 q = q.order_by(ChangesetComment.created_on)
309 return q.all()
308 return q.all()
310
309
311 def get_url(self, comment):
310 def get_url(self, comment):
312 comment = self.__get_commit_comment(comment)
311 comment = self.__get_commit_comment(comment)
313 if comment.pull_request:
312 if comment.pull_request:
314 return h.url(
313 return h.url(
315 'pullrequest_show',
314 'pullrequest_show',
316 repo_name=comment.pull_request.target_repo.repo_name,
315 repo_name=comment.pull_request.target_repo.repo_name,
317 pull_request_id=comment.pull_request.pull_request_id,
316 pull_request_id=comment.pull_request.pull_request_id,
318 anchor='comment-%s' % comment.comment_id,
317 anchor='comment-%s' % comment.comment_id,
319 qualified=True,)
318 qualified=True,)
320 else:
319 else:
321 return h.url(
320 return h.url(
322 'changeset_home',
321 'changeset_home',
323 repo_name=comment.repo.repo_name,
322 repo_name=comment.repo.repo_name,
324 revision=comment.revision,
323 revision=comment.revision,
325 anchor='comment-%s' % comment.comment_id,
324 anchor='comment-%s' % comment.comment_id,
326 qualified=True,)
325 qualified=True,)
327
326
328 def get_comments(self, repo_id, revision=None, pull_request=None):
327 def get_comments(self, repo_id, revision=None, pull_request=None):
329 """
328 """
330 Gets main comments based on revision or pull_request_id
329 Gets main comments based on revision or pull_request_id
331
330
332 :param repo_id:
331 :param repo_id:
333 :param revision:
332 :param revision:
334 :param pull_request:
333 :param pull_request:
335 """
334 """
336
335
337 q = ChangesetComment.query()\
336 q = ChangesetComment.query()\
338 .filter(ChangesetComment.repo_id == repo_id)\
337 .filter(ChangesetComment.repo_id == repo_id)\
339 .filter(ChangesetComment.line_no == None)\
338 .filter(ChangesetComment.line_no == None)\
340 .filter(ChangesetComment.f_path == None)
339 .filter(ChangesetComment.f_path == None)
341 if revision:
340 if revision:
342 q = q.filter(ChangesetComment.revision == revision)
341 q = q.filter(ChangesetComment.revision == revision)
343 elif pull_request:
342 elif pull_request:
344 pull_request = self.__get_pull_request(pull_request)
343 pull_request = self.__get_pull_request(pull_request)
345 q = q.filter(ChangesetComment.pull_request == pull_request)
344 q = q.filter(ChangesetComment.pull_request == pull_request)
346 else:
345 else:
347 raise Exception('Please specify commit or pull_request')
346 raise Exception('Please specify commit or pull_request')
348 q = q.order_by(ChangesetComment.created_on)
347 q = q.order_by(ChangesetComment.created_on)
349 return q.all()
348 return q.all()
350
349
351 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
350 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
352 q = self._get_inline_comments_query(repo_id, revision, pull_request)
351 q = self._get_inline_comments_query(repo_id, revision, pull_request)
353 return self._group_comments_by_path_and_line_number(q)
352 return self._group_comments_by_path_and_line_number(q)
354
353
355 def get_outdated_comments(self, repo_id, pull_request):
354 def get_outdated_comments(self, repo_id, pull_request):
356 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
355 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
357 # of a pull request.
356 # of a pull request.
358 q = self._all_inline_comments_of_pull_request(pull_request)
357 q = self._all_inline_comments_of_pull_request(pull_request)
359 q = q.filter(
358 q = q.filter(
360 ChangesetComment.display_state ==
359 ChangesetComment.display_state ==
361 ChangesetComment.COMMENT_OUTDATED
360 ChangesetComment.COMMENT_OUTDATED
362 ).order_by(ChangesetComment.comment_id.asc())
361 ).order_by(ChangesetComment.comment_id.asc())
363
362
364 return self._group_comments_by_path_and_line_number(q)
363 return self._group_comments_by_path_and_line_number(q)
365
364
366 def _get_inline_comments_query(self, repo_id, revision, pull_request):
365 def _get_inline_comments_query(self, repo_id, revision, pull_request):
367 # TODO: johbo: Split this into two methods: One for PR and one for
366 # TODO: johbo: Split this into two methods: One for PR and one for
368 # commit.
367 # commit.
369 if revision:
368 if revision:
370 q = Session().query(ChangesetComment).filter(
369 q = Session().query(ChangesetComment).filter(
371 ChangesetComment.repo_id == repo_id,
370 ChangesetComment.repo_id == repo_id,
372 ChangesetComment.line_no != null(),
371 ChangesetComment.line_no != null(),
373 ChangesetComment.f_path != null(),
372 ChangesetComment.f_path != null(),
374 ChangesetComment.revision == revision)
373 ChangesetComment.revision == revision)
375
374
376 elif pull_request:
375 elif pull_request:
377 pull_request = self.__get_pull_request(pull_request)
376 pull_request = self.__get_pull_request(pull_request)
378 if ChangesetCommentsModel.use_outdated_comments(pull_request):
377 if ChangesetCommentsModel.use_outdated_comments(pull_request):
379 q = self._visible_inline_comments_of_pull_request(pull_request)
378 q = self._visible_inline_comments_of_pull_request(pull_request)
380 else:
379 else:
381 q = self._all_inline_comments_of_pull_request(pull_request)
380 q = self._all_inline_comments_of_pull_request(pull_request)
382
381
383 else:
382 else:
384 raise Exception('Please specify commit or pull_request_id')
383 raise Exception('Please specify commit or pull_request_id')
385 q = q.order_by(ChangesetComment.comment_id.asc())
384 q = q.order_by(ChangesetComment.comment_id.asc())
386 return q
385 return q
387
386
388 def _group_comments_by_path_and_line_number(self, q):
387 def _group_comments_by_path_and_line_number(self, q):
389 comments = q.all()
388 comments = q.all()
390 paths = collections.defaultdict(lambda: collections.defaultdict(list))
389 paths = collections.defaultdict(lambda: collections.defaultdict(list))
391 for co in comments:
390 for co in comments:
392 paths[co.f_path][co.line_no].append(co)
391 paths[co.f_path][co.line_no].append(co)
393 return paths
392 return paths
394
393
395 @classmethod
394 @classmethod
396 def needed_extra_diff_context(cls):
395 def needed_extra_diff_context(cls):
397 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
396 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
398
397
399 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
398 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
400 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
399 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
401 return
400 return
402
401
403 comments = self._visible_inline_comments_of_pull_request(pull_request)
402 comments = self._visible_inline_comments_of_pull_request(pull_request)
404 comments_to_outdate = comments.all()
403 comments_to_outdate = comments.all()
405
404
406 for comment in comments_to_outdate:
405 for comment in comments_to_outdate:
407 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
406 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
408
407
409 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
408 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
410 diff_line = _parse_comment_line_number(comment.line_no)
409 diff_line = _parse_comment_line_number(comment.line_no)
411
410
412 try:
411 try:
413 old_context = old_diff_proc.get_context_of_line(
412 old_context = old_diff_proc.get_context_of_line(
414 path=comment.f_path, diff_line=diff_line)
413 path=comment.f_path, diff_line=diff_line)
415 new_context = new_diff_proc.get_context_of_line(
414 new_context = new_diff_proc.get_context_of_line(
416 path=comment.f_path, diff_line=diff_line)
415 path=comment.f_path, diff_line=diff_line)
417 except (diffs.LineNotInDiffException,
416 except (diffs.LineNotInDiffException,
418 diffs.FileNotInDiffException):
417 diffs.FileNotInDiffException):
419 comment.display_state = ChangesetComment.COMMENT_OUTDATED
418 comment.display_state = ChangesetComment.COMMENT_OUTDATED
420 return
419 return
421
420
422 if old_context == new_context:
421 if old_context == new_context:
423 return
422 return
424
423
425 if self._should_relocate_diff_line(diff_line):
424 if self._should_relocate_diff_line(diff_line):
426 new_diff_lines = new_diff_proc.find_context(
425 new_diff_lines = new_diff_proc.find_context(
427 path=comment.f_path, context=old_context,
426 path=comment.f_path, context=old_context,
428 offset=self.DIFF_CONTEXT_BEFORE)
427 offset=self.DIFF_CONTEXT_BEFORE)
429 if not new_diff_lines:
428 if not new_diff_lines:
430 comment.display_state = ChangesetComment.COMMENT_OUTDATED
429 comment.display_state = ChangesetComment.COMMENT_OUTDATED
431 else:
430 else:
432 new_diff_line = self._choose_closest_diff_line(
431 new_diff_line = self._choose_closest_diff_line(
433 diff_line, new_diff_lines)
432 diff_line, new_diff_lines)
434 comment.line_no = _diff_to_comment_line_number(new_diff_line)
433 comment.line_no = _diff_to_comment_line_number(new_diff_line)
435 else:
434 else:
436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
435 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437
436
438 def _should_relocate_diff_line(self, diff_line):
437 def _should_relocate_diff_line(self, diff_line):
439 """
438 """
440 Checks if relocation shall be tried for the given `diff_line`.
439 Checks if relocation shall be tried for the given `diff_line`.
441
440
442 If a comment points into the first lines, then we can have a situation
441 If a comment points into the first lines, then we can have a situation
443 that after an update another line has been added on top. In this case
442 that after an update another line has been added on top. In this case
444 we would find the context still and move the comment around. This
443 we would find the context still and move the comment around. This
445 would be wrong.
444 would be wrong.
446 """
445 """
447 should_relocate = (
446 should_relocate = (
448 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
447 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
449 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
448 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
450 return should_relocate
449 return should_relocate
451
450
452 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
451 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
453 candidate = new_diff_lines[0]
452 candidate = new_diff_lines[0]
454 best_delta = _diff_line_delta(diff_line, candidate)
453 best_delta = _diff_line_delta(diff_line, candidate)
455 for new_diff_line in new_diff_lines[1:]:
454 for new_diff_line in new_diff_lines[1:]:
456 delta = _diff_line_delta(diff_line, new_diff_line)
455 delta = _diff_line_delta(diff_line, new_diff_line)
457 if delta < best_delta:
456 if delta < best_delta:
458 candidate = new_diff_line
457 candidate = new_diff_line
459 best_delta = delta
458 best_delta = delta
460 return candidate
459 return candidate
461
460
462 def _visible_inline_comments_of_pull_request(self, pull_request):
461 def _visible_inline_comments_of_pull_request(self, pull_request):
463 comments = self._all_inline_comments_of_pull_request(pull_request)
462 comments = self._all_inline_comments_of_pull_request(pull_request)
464 comments = comments.filter(
463 comments = comments.filter(
465 coalesce(ChangesetComment.display_state, '') !=
464 coalesce(ChangesetComment.display_state, '') !=
466 ChangesetComment.COMMENT_OUTDATED)
465 ChangesetComment.COMMENT_OUTDATED)
467 return comments
466 return comments
468
467
469 def _all_inline_comments_of_pull_request(self, pull_request):
468 def _all_inline_comments_of_pull_request(self, pull_request):
470 comments = Session().query(ChangesetComment)\
469 comments = Session().query(ChangesetComment)\
471 .filter(ChangesetComment.line_no != None)\
470 .filter(ChangesetComment.line_no != None)\
472 .filter(ChangesetComment.f_path != None)\
471 .filter(ChangesetComment.f_path != None)\
473 .filter(ChangesetComment.pull_request == pull_request)
472 .filter(ChangesetComment.pull_request == pull_request)
474 return comments
473 return comments
475
474
476 @staticmethod
475 @staticmethod
477 def use_outdated_comments(pull_request):
476 def use_outdated_comments(pull_request):
478 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
477 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
479 settings = settings_model.get_general_settings()
478 settings = settings_model.get_general_settings()
480 return settings.get('rhodecode_use_outdated_comments', False)
479 return settings.get('rhodecode_use_outdated_comments', False)
481
480
482
481
483 def _parse_comment_line_number(line_no):
482 def _parse_comment_line_number(line_no):
484 """
483 """
485 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
484 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
486 """
485 """
487 old_line = None
486 old_line = None
488 new_line = None
487 new_line = None
489 if line_no.startswith('o'):
488 if line_no.startswith('o'):
490 old_line = int(line_no[1:])
489 old_line = int(line_no[1:])
491 elif line_no.startswith('n'):
490 elif line_no.startswith('n'):
492 new_line = int(line_no[1:])
491 new_line = int(line_no[1:])
493 else:
492 else:
494 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
493 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
495 return diffs.DiffLineNumber(old_line, new_line)
494 return diffs.DiffLineNumber(old_line, new_line)
496
495
497
496
498 def _diff_to_comment_line_number(diff_line):
497 def _diff_to_comment_line_number(diff_line):
499 if diff_line.new is not None:
498 if diff_line.new is not None:
500 return u'n{}'.format(diff_line.new)
499 return u'n{}'.format(diff_line.new)
501 elif diff_line.old is not None:
500 elif diff_line.old is not None:
502 return u'o{}'.format(diff_line.old)
501 return u'o{}'.format(diff_line.old)
503 return u''
502 return u''
504
503
505
504
506 def _diff_line_delta(a, b):
505 def _diff_line_delta(a, b):
507 if None not in (a.new, b.new):
506 if None not in (a.new, b.new):
508 return abs(a.new - b.new)
507 return abs(a.new - b.new)
509 elif None not in (a.old, b.old):
508 elif None not in (a.old, b.old):
510 return abs(a.old - b.old)
509 return abs(a.old - b.old)
511 else:
510 else:
512 raise ValueError(
511 raise ValueError(
513 "Cannot compute delta between {} and {}".format(a, b))
512 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now