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