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