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