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