##// END OF EJS Templates
notifications: skip double rendering just to generate email title/desc
milka -
r4560:55f25c6b default
parent child Browse files
Show More
@@ -1,848 +1,845 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 false,
40 false,
41 ChangesetComment,
41 ChangesetComment,
42 User,
42 User,
43 Notification,
43 Notification,
44 PullRequest,
44 PullRequest,
45 AttributeDict,
45 AttributeDict,
46 ChangesetCommentHistory,
46 ChangesetCommentHistory,
47 )
47 )
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
52 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
59
59
60 cls = ChangesetComment
60 cls = ChangesetComment
61
61
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
63 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
64
64
65 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
66 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
67
67
68 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
69 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
70
70
71 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
72 user_objects = []
72 user_objects = []
73 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
75 if user_obj:
75 if user_obj:
76 user_objects.append(user_obj)
76 user_objects.append(user_obj)
77 return user_objects
77 return user_objects
78
78
79 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
80 request = request or get_current_request()
80 request = request or get_current_request()
81
81
82 try:
82 try:
83 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
84 except AttributeError:
84 except AttributeError:
85 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
86 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
87 except Exception:
87 except Exception:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 return global_renderer
89 return global_renderer
90
90
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
93
93
94 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not include_drafts:
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
173 todos = todos.filter(ChangesetComment.draft == false())
174
174
175 if not show_outdated:
175 if not show_outdated:
176 todos = todos.filter(
176 todos = todos.filter(
177 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
178 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
179
179
180 todos = todos.all()
180 todos = todos.all()
181
181
182 return todos
182 return todos
183
183
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185
185
186 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
187 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
188 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
189 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
190 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
191
191
192 if not include_drafts:
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
193 todos = todos.filter(ChangesetComment.draft == false())
194
194
195 if not show_outdated:
195 if not show_outdated:
196 todos = todos.filter(
196 todos = todos.filter(
197 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
198 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
199
199
200 todos = todos.all()
200 todos = todos.all()
201
201
202 return todos
202 return todos
203
203
204 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
204 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
205
205
206 todos = Session().query(ChangesetComment) \
206 todos = Session().query(ChangesetComment) \
207 .filter(ChangesetComment.revision == commit_id) \
207 .filter(ChangesetComment.revision == commit_id) \
208 .filter(ChangesetComment.resolved_by == None) \
208 .filter(ChangesetComment.resolved_by == None) \
209 .filter(ChangesetComment.comment_type
209 .filter(ChangesetComment.comment_type
210 == ChangesetComment.COMMENT_TYPE_TODO)
210 == ChangesetComment.COMMENT_TYPE_TODO)
211
211
212 if not include_drafts:
212 if not include_drafts:
213 todos = todos.filter(ChangesetComment.draft == false())
213 todos = todos.filter(ChangesetComment.draft == false())
214
214
215 if not show_outdated:
215 if not show_outdated:
216 todos = todos.filter(
216 todos = todos.filter(
217 coalesce(ChangesetComment.display_state, '') !=
217 coalesce(ChangesetComment.display_state, '') !=
218 ChangesetComment.COMMENT_OUTDATED)
218 ChangesetComment.COMMENT_OUTDATED)
219
219
220 todos = todos.all()
220 todos = todos.all()
221
221
222 return todos
222 return todos
223
223
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
225
225
226 todos = Session().query(ChangesetComment) \
226 todos = Session().query(ChangesetComment) \
227 .filter(ChangesetComment.revision == commit_id) \
227 .filter(ChangesetComment.revision == commit_id) \
228 .filter(ChangesetComment.resolved_by != None) \
228 .filter(ChangesetComment.resolved_by != None) \
229 .filter(ChangesetComment.comment_type
229 .filter(ChangesetComment.comment_type
230 == ChangesetComment.COMMENT_TYPE_TODO)
230 == ChangesetComment.COMMENT_TYPE_TODO)
231
231
232 if not include_drafts:
232 if not include_drafts:
233 todos = todos.filter(ChangesetComment.draft == false())
233 todos = todos.filter(ChangesetComment.draft == false())
234
234
235 if not show_outdated:
235 if not show_outdated:
236 todos = todos.filter(
236 todos = todos.filter(
237 coalesce(ChangesetComment.display_state, '') !=
237 coalesce(ChangesetComment.display_state, '') !=
238 ChangesetComment.COMMENT_OUTDATED)
238 ChangesetComment.COMMENT_OUTDATED)
239
239
240 todos = todos.all()
240 todos = todos.all()
241
241
242 return todos
242 return todos
243
243
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
245 inline_comments = Session().query(ChangesetComment) \
245 inline_comments = Session().query(ChangesetComment) \
246 .filter(ChangesetComment.line_no != None) \
246 .filter(ChangesetComment.line_no != None) \
247 .filter(ChangesetComment.f_path != None) \
247 .filter(ChangesetComment.f_path != None) \
248 .filter(ChangesetComment.revision == commit_id)
248 .filter(ChangesetComment.revision == commit_id)
249
249
250 if not include_drafts:
250 if not include_drafts:
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252
252
253 inline_comments = inline_comments.all()
253 inline_comments = inline_comments.all()
254 return inline_comments
254 return inline_comments
255
255
256 def _log_audit_action(self, action, action_data, auth_user, comment):
256 def _log_audit_action(self, action, action_data, auth_user, comment):
257 audit_logger.store(
257 audit_logger.store(
258 action=action,
258 action=action,
259 action_data=action_data,
259 action_data=action_data,
260 user=auth_user,
260 user=auth_user,
261 repo=comment.repo)
261 repo=comment.repo)
262
262
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
264 f_path=None, line_no=None, status_change=None,
264 f_path=None, line_no=None, status_change=None,
265 status_change_type=None, comment_type=None, is_draft=False,
265 status_change_type=None, comment_type=None, is_draft=False,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
267 renderer=None, auth_user=None, extra_recipients=None):
267 renderer=None, auth_user=None, extra_recipients=None):
268 """
268 """
269 Creates new comment for commit or pull request.
269 Creates new comment for commit or pull request.
270 IF status_change is not none this comment is associated with a
270 IF status_change is not none this comment is associated with a
271 status change of commit or commit associated with pull request
271 status change of commit or commit associated with pull request
272
272
273 :param text:
273 :param text:
274 :param repo:
274 :param repo:
275 :param user:
275 :param user:
276 :param commit_id:
276 :param commit_id:
277 :param pull_request:
277 :param pull_request:
278 :param f_path:
278 :param f_path:
279 :param line_no:
279 :param line_no:
280 :param status_change: Label for status change
280 :param status_change: Label for status change
281 :param comment_type: Type of comment
281 :param comment_type: Type of comment
282 :param is_draft: is comment a draft only
282 :param is_draft: is comment a draft only
283 :param resolves_comment_id: id of comment which this one will resolve
283 :param resolves_comment_id: id of comment which this one will resolve
284 :param status_change_type: type of status change
284 :param status_change_type: type of status change
285 :param closing_pr:
285 :param closing_pr:
286 :param send_email:
286 :param send_email:
287 :param renderer: pick renderer for this comment
287 :param renderer: pick renderer for this comment
288 :param auth_user: current authenticated user calling this method
288 :param auth_user: current authenticated user calling this method
289 :param extra_recipients: list of extra users to be added to recipients
289 :param extra_recipients: list of extra users to be added to recipients
290 """
290 """
291
291
292 if not text:
292 if not text:
293 log.warning('Missing text for comment, skipping...')
293 log.warning('Missing text for comment, skipping...')
294 return
294 return
295 request = get_current_request()
295 request = get_current_request()
296 _ = request.translate
296 _ = request.translate
297
297
298 if not renderer:
298 if not renderer:
299 renderer = self._get_renderer(request=request)
299 renderer = self._get_renderer(request=request)
300
300
301 repo = self._get_repo(repo)
301 repo = self._get_repo(repo)
302 user = self._get_user(user)
302 user = self._get_user(user)
303 auth_user = auth_user or user
303 auth_user = auth_user or user
304
304
305 schema = comment_schema.CommentSchema()
305 schema = comment_schema.CommentSchema()
306 validated_kwargs = schema.deserialize(dict(
306 validated_kwargs = schema.deserialize(dict(
307 comment_body=text,
307 comment_body=text,
308 comment_type=comment_type,
308 comment_type=comment_type,
309 is_draft=is_draft,
309 is_draft=is_draft,
310 comment_file=f_path,
310 comment_file=f_path,
311 comment_line=line_no,
311 comment_line=line_no,
312 renderer_type=renderer,
312 renderer_type=renderer,
313 status_change=status_change_type,
313 status_change=status_change_type,
314 resolves_comment_id=resolves_comment_id,
314 resolves_comment_id=resolves_comment_id,
315 repo=repo.repo_id,
315 repo=repo.repo_id,
316 user=user.user_id,
316 user=user.user_id,
317 ))
317 ))
318 is_draft = validated_kwargs['is_draft']
318 is_draft = validated_kwargs['is_draft']
319
319
320 comment = ChangesetComment()
320 comment = ChangesetComment()
321 comment.renderer = validated_kwargs['renderer_type']
321 comment.renderer = validated_kwargs['renderer_type']
322 comment.text = validated_kwargs['comment_body']
322 comment.text = validated_kwargs['comment_body']
323 comment.f_path = validated_kwargs['comment_file']
323 comment.f_path = validated_kwargs['comment_file']
324 comment.line_no = validated_kwargs['comment_line']
324 comment.line_no = validated_kwargs['comment_line']
325 comment.comment_type = validated_kwargs['comment_type']
325 comment.comment_type = validated_kwargs['comment_type']
326 comment.draft = is_draft
326 comment.draft = is_draft
327
327
328 comment.repo = repo
328 comment.repo = repo
329 comment.author = user
329 comment.author = user
330 resolved_comment = self.__get_commit_comment(
330 resolved_comment = self.__get_commit_comment(
331 validated_kwargs['resolves_comment_id'])
331 validated_kwargs['resolves_comment_id'])
332 # check if the comment actually belongs to this PR
332 # check if the comment actually belongs to this PR
333 if resolved_comment and resolved_comment.pull_request and \
333 if resolved_comment and resolved_comment.pull_request and \
334 resolved_comment.pull_request != pull_request:
334 resolved_comment.pull_request != pull_request:
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
336 resolved_comment)
336 resolved_comment)
337 # comment not bound to this pull request, forbid
337 # comment not bound to this pull request, forbid
338 resolved_comment = None
338 resolved_comment = None
339
339
340 elif resolved_comment and resolved_comment.repo and \
340 elif resolved_comment and resolved_comment.repo and \
341 resolved_comment.repo != repo:
341 resolved_comment.repo != repo:
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 resolved_comment)
343 resolved_comment)
344 # comment not bound to this repo, forbid
344 # comment not bound to this repo, forbid
345 resolved_comment = None
345 resolved_comment = None
346
346
347 comment.resolved_comment = resolved_comment
347 comment.resolved_comment = resolved_comment
348
348
349 pull_request_id = pull_request
349 pull_request_id = pull_request
350
350
351 commit_obj = None
351 commit_obj = None
352 pull_request_obj = None
352 pull_request_obj = None
353
353
354 if commit_id:
354 if commit_id:
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
356 # do a lookup, so we don't pass something bad here
356 # do a lookup, so we don't pass something bad here
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
358 comment.revision = commit_obj.raw_id
358 comment.revision = commit_obj.raw_id
359
359
360 elif pull_request_id:
360 elif pull_request_id:
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
362 pull_request_obj = self.__get_pull_request(pull_request_id)
362 pull_request_obj = self.__get_pull_request(pull_request_id)
363 comment.pull_request = pull_request_obj
363 comment.pull_request = pull_request_obj
364 else:
364 else:
365 raise Exception('Please specify commit or pull_request_id')
365 raise Exception('Please specify commit or pull_request_id')
366
366
367 Session().add(comment)
367 Session().add(comment)
368 Session().flush()
368 Session().flush()
369 kwargs = {
369 kwargs = {
370 'user': user,
370 'user': user,
371 'renderer_type': renderer,
371 'renderer_type': renderer,
372 'repo_name': repo.repo_name,
372 'repo_name': repo.repo_name,
373 'status_change': status_change,
373 'status_change': status_change,
374 'status_change_type': status_change_type,
374 'status_change_type': status_change_type,
375 'comment_body': text,
375 'comment_body': text,
376 'comment_file': f_path,
376 'comment_file': f_path,
377 'comment_line': line_no,
377 'comment_line': line_no,
378 'comment_type': comment_type or 'note',
378 'comment_type': comment_type or 'note',
379 'comment_id': comment.comment_id
379 'comment_id': comment.comment_id
380 }
380 }
381
381
382 if commit_obj:
382 if commit_obj:
383 recipients = ChangesetComment.get_users(
383 recipients = ChangesetComment.get_users(
384 revision=commit_obj.raw_id)
384 revision=commit_obj.raw_id)
385 # add commit author if it's in RhodeCode system
385 # add commit author if it's in RhodeCode system
386 cs_author = User.get_from_cs_author(commit_obj.author)
386 cs_author = User.get_from_cs_author(commit_obj.author)
387 if not cs_author:
387 if not cs_author:
388 # use repo owner if we cannot extract the author correctly
388 # use repo owner if we cannot extract the author correctly
389 cs_author = repo.user
389 cs_author = repo.user
390 recipients += [cs_author]
390 recipients += [cs_author]
391
391
392 commit_comment_url = self.get_url(comment, request=request)
392 commit_comment_url = self.get_url(comment, request=request)
393 commit_comment_reply_url = self.get_url(
393 commit_comment_reply_url = self.get_url(
394 comment, request=request,
394 comment, request=request,
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
396
396
397 target_repo_url = h.link_to(
397 target_repo_url = h.link_to(
398 repo.repo_name,
398 repo.repo_name,
399 h.route_url('repo_summary', repo_name=repo.repo_name))
399 h.route_url('repo_summary', repo_name=repo.repo_name))
400
400
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
402 commit_id=commit_id)
402 commit_id=commit_id)
403
403
404 # commit specifics
404 # commit specifics
405 kwargs.update({
405 kwargs.update({
406 'commit': commit_obj,
406 'commit': commit_obj,
407 'commit_message': commit_obj.message,
407 'commit_message': commit_obj.message,
408 'commit_target_repo_url': target_repo_url,
408 'commit_target_repo_url': target_repo_url,
409 'commit_comment_url': commit_comment_url,
409 'commit_comment_url': commit_comment_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
411 'commit_url': commit_url,
411 'commit_url': commit_url,
412 'thread_ids': [commit_url, commit_comment_url],
412 'thread_ids': [commit_url, commit_comment_url],
413 })
413 })
414
414
415 elif pull_request_obj:
415 elif pull_request_obj:
416 # get the current participants of this pull request
416 # get the current participants of this pull request
417 recipients = ChangesetComment.get_users(
417 recipients = ChangesetComment.get_users(
418 pull_request_id=pull_request_obj.pull_request_id)
418 pull_request_id=pull_request_obj.pull_request_id)
419 # add pull request author
419 # add pull request author
420 recipients += [pull_request_obj.author]
420 recipients += [pull_request_obj.author]
421
421
422 # add the reviewers to notification
422 # add the reviewers to notification
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
424
424
425 pr_target_repo = pull_request_obj.target_repo
425 pr_target_repo = pull_request_obj.target_repo
426 pr_source_repo = pull_request_obj.source_repo
426 pr_source_repo = pull_request_obj.source_repo
427
427
428 pr_comment_url = self.get_url(comment, request=request)
428 pr_comment_url = self.get_url(comment, request=request)
429 pr_comment_reply_url = self.get_url(
429 pr_comment_reply_url = self.get_url(
430 comment, request=request,
430 comment, request=request,
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
432
432
433 pr_url = h.route_url(
433 pr_url = h.route_url(
434 'pullrequest_show',
434 'pullrequest_show',
435 repo_name=pr_target_repo.repo_name,
435 repo_name=pr_target_repo.repo_name,
436 pull_request_id=pull_request_obj.pull_request_id, )
436 pull_request_id=pull_request_obj.pull_request_id, )
437
437
438 # set some variables for email notification
438 # set some variables for email notification
439 pr_target_repo_url = h.route_url(
439 pr_target_repo_url = h.route_url(
440 'repo_summary', repo_name=pr_target_repo.repo_name)
440 'repo_summary', repo_name=pr_target_repo.repo_name)
441
441
442 pr_source_repo_url = h.route_url(
442 pr_source_repo_url = h.route_url(
443 'repo_summary', repo_name=pr_source_repo.repo_name)
443 'repo_summary', repo_name=pr_source_repo.repo_name)
444
444
445 # pull request specifics
445 # pull request specifics
446 kwargs.update({
446 kwargs.update({
447 'pull_request': pull_request_obj,
447 'pull_request': pull_request_obj,
448 'pr_id': pull_request_obj.pull_request_id,
448 'pr_id': pull_request_obj.pull_request_id,
449 'pull_request_url': pr_url,
449 'pull_request_url': pr_url,
450 'pull_request_target_repo': pr_target_repo,
450 'pull_request_target_repo': pr_target_repo,
451 'pull_request_target_repo_url': pr_target_repo_url,
451 'pull_request_target_repo_url': pr_target_repo_url,
452 'pull_request_source_repo': pr_source_repo,
452 'pull_request_source_repo': pr_source_repo,
453 'pull_request_source_repo_url': pr_source_repo_url,
453 'pull_request_source_repo_url': pr_source_repo_url,
454 'pr_comment_url': pr_comment_url,
454 'pr_comment_url': pr_comment_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
456 'pr_closing': closing_pr,
456 'pr_closing': closing_pr,
457 'thread_ids': [pr_url, pr_comment_url],
457 'thread_ids': [pr_url, pr_comment_url],
458 })
458 })
459
459
460 if send_email:
460 if send_email:
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
462 # pre-generate the subject for notification itself
463 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
464 notification_type, **kwargs)
465
462
466 mention_recipients = set(
463 mention_recipients = set(
467 self._extract_mentions(text)).difference(recipients)
464 self._extract_mentions(text)).difference(recipients)
468
465
469 # create notification objects, and emails
466 # create notification objects, and emails
470 NotificationModel().create(
467 NotificationModel().create(
471 created_by=user,
468 created_by=user,
472 notification_subject=subject,
469 notification_subject='', # Filled in based on the notification_type
473 notification_body=body_plaintext,
470 notification_body='', # Filled in based on the notification_type
474 notification_type=notification_type,
471 notification_type=notification_type,
475 recipients=recipients,
472 recipients=recipients,
476 mention_recipients=mention_recipients,
473 mention_recipients=mention_recipients,
477 email_kwargs=kwargs,
474 email_kwargs=kwargs,
478 )
475 )
479
476
480 Session().flush()
477 Session().flush()
481 if comment.pull_request:
478 if comment.pull_request:
482 action = 'repo.pull_request.comment.create'
479 action = 'repo.pull_request.comment.create'
483 else:
480 else:
484 action = 'repo.commit.comment.create'
481 action = 'repo.commit.comment.create'
485
482
486 if not is_draft:
483 if not is_draft:
487 comment_data = comment.get_api_data()
484 comment_data = comment.get_api_data()
488
485
489 self._log_audit_action(
486 self._log_audit_action(
490 action, {'data': comment_data}, auth_user, comment)
487 action, {'data': comment_data}, auth_user, comment)
491
488
492 return comment
489 return comment
493
490
494 def edit(self, comment_id, text, auth_user, version):
491 def edit(self, comment_id, text, auth_user, version):
495 """
492 """
496 Change existing comment for commit or pull request.
493 Change existing comment for commit or pull request.
497
494
498 :param comment_id:
495 :param comment_id:
499 :param text:
496 :param text:
500 :param auth_user: current authenticated user calling this method
497 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
498 :param version: last comment version
502 """
499 """
503 if not text:
500 if not text:
504 log.warning('Missing text for comment, skipping...')
501 log.warning('Missing text for comment, skipping...')
505 return
502 return
506
503
507 comment = ChangesetComment.get(comment_id)
504 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
505 old_comment_text = comment.text
509 comment.text = text
506 comment.text = text
510 comment.modified_at = datetime.datetime.now()
507 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
508 version = safe_int(version)
512
509
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
510 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
511 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
512 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
513
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
514 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
515 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
516 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
517 comment_version-1, # -1 since note above
521 version
518 version
522 )
519 )
523 )
520 )
524 raise CommentVersionMismatch()
521 raise CommentVersionMismatch()
525
522
526 comment_history = ChangesetCommentHistory()
523 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
524 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
525 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
526 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
527 comment_history.text = old_comment_text
531 # TODO add email notification
528 # TODO add email notification
532 Session().add(comment_history)
529 Session().add(comment_history)
533 Session().add(comment)
530 Session().add(comment)
534 Session().flush()
531 Session().flush()
535
532
536 if comment.pull_request:
533 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
534 action = 'repo.pull_request.comment.edit'
538 else:
535 else:
539 action = 'repo.commit.comment.edit'
536 action = 'repo.commit.comment.edit'
540
537
541 comment_data = comment.get_api_data()
538 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
539 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
540 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
541 action, {'data': comment_data}, auth_user, comment)
545
542
546 return comment_history
543 return comment_history
547
544
548 def delete(self, comment, auth_user):
545 def delete(self, comment, auth_user):
549 """
546 """
550 Deletes given comment
547 Deletes given comment
551 """
548 """
552 comment = self.__get_commit_comment(comment)
549 comment = self.__get_commit_comment(comment)
553 old_data = comment.get_api_data()
550 old_data = comment.get_api_data()
554 Session().delete(comment)
551 Session().delete(comment)
555
552
556 if comment.pull_request:
553 if comment.pull_request:
557 action = 'repo.pull_request.comment.delete'
554 action = 'repo.pull_request.comment.delete'
558 else:
555 else:
559 action = 'repo.commit.comment.delete'
556 action = 'repo.commit.comment.delete'
560
557
561 self._log_audit_action(
558 self._log_audit_action(
562 action, {'old_data': old_data}, auth_user, comment)
559 action, {'old_data': old_data}, auth_user, comment)
563
560
564 return comment
561 return comment
565
562
566 def get_all_comments(self, repo_id, revision=None, pull_request=None,
563 def get_all_comments(self, repo_id, revision=None, pull_request=None,
567 include_drafts=True, count_only=False):
564 include_drafts=True, count_only=False):
568 q = ChangesetComment.query()\
565 q = ChangesetComment.query()\
569 .filter(ChangesetComment.repo_id == repo_id)
566 .filter(ChangesetComment.repo_id == repo_id)
570 if revision:
567 if revision:
571 q = q.filter(ChangesetComment.revision == revision)
568 q = q.filter(ChangesetComment.revision == revision)
572 elif pull_request:
569 elif pull_request:
573 pull_request = self.__get_pull_request(pull_request)
570 pull_request = self.__get_pull_request(pull_request)
574 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
571 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
575 else:
572 else:
576 raise Exception('Please specify commit or pull_request')
573 raise Exception('Please specify commit or pull_request')
577 if not include_drafts:
574 if not include_drafts:
578 q = q.filter(ChangesetComment.draft == false())
575 q = q.filter(ChangesetComment.draft == false())
579 q = q.order_by(ChangesetComment.created_on)
576 q = q.order_by(ChangesetComment.created_on)
580 if count_only:
577 if count_only:
581 return q.count()
578 return q.count()
582
579
583 return q.all()
580 return q.all()
584
581
585 def get_url(self, comment, request=None, permalink=False, anchor=None):
582 def get_url(self, comment, request=None, permalink=False, anchor=None):
586 if not request:
583 if not request:
587 request = get_current_request()
584 request = get_current_request()
588
585
589 comment = self.__get_commit_comment(comment)
586 comment = self.__get_commit_comment(comment)
590 if anchor is None:
587 if anchor is None:
591 anchor = 'comment-{}'.format(comment.comment_id)
588 anchor = 'comment-{}'.format(comment.comment_id)
592
589
593 if comment.pull_request:
590 if comment.pull_request:
594 pull_request = comment.pull_request
591 pull_request = comment.pull_request
595 if permalink:
592 if permalink:
596 return request.route_url(
593 return request.route_url(
597 'pull_requests_global',
594 'pull_requests_global',
598 pull_request_id=pull_request.pull_request_id,
595 pull_request_id=pull_request.pull_request_id,
599 _anchor=anchor)
596 _anchor=anchor)
600 else:
597 else:
601 return request.route_url(
598 return request.route_url(
602 'pullrequest_show',
599 'pullrequest_show',
603 repo_name=safe_str(pull_request.target_repo.repo_name),
600 repo_name=safe_str(pull_request.target_repo.repo_name),
604 pull_request_id=pull_request.pull_request_id,
601 pull_request_id=pull_request.pull_request_id,
605 _anchor=anchor)
602 _anchor=anchor)
606
603
607 else:
604 else:
608 repo = comment.repo
605 repo = comment.repo
609 commit_id = comment.revision
606 commit_id = comment.revision
610
607
611 if permalink:
608 if permalink:
612 return request.route_url(
609 return request.route_url(
613 'repo_commit', repo_name=safe_str(repo.repo_id),
610 'repo_commit', repo_name=safe_str(repo.repo_id),
614 commit_id=commit_id,
611 commit_id=commit_id,
615 _anchor=anchor)
612 _anchor=anchor)
616
613
617 else:
614 else:
618 return request.route_url(
615 return request.route_url(
619 'repo_commit', repo_name=safe_str(repo.repo_name),
616 'repo_commit', repo_name=safe_str(repo.repo_name),
620 commit_id=commit_id,
617 commit_id=commit_id,
621 _anchor=anchor)
618 _anchor=anchor)
622
619
623 def get_comments(self, repo_id, revision=None, pull_request=None):
620 def get_comments(self, repo_id, revision=None, pull_request=None):
624 """
621 """
625 Gets main comments based on revision or pull_request_id
622 Gets main comments based on revision or pull_request_id
626
623
627 :param repo_id:
624 :param repo_id:
628 :param revision:
625 :param revision:
629 :param pull_request:
626 :param pull_request:
630 """
627 """
631
628
632 q = ChangesetComment.query()\
629 q = ChangesetComment.query()\
633 .filter(ChangesetComment.repo_id == repo_id)\
630 .filter(ChangesetComment.repo_id == repo_id)\
634 .filter(ChangesetComment.line_no == None)\
631 .filter(ChangesetComment.line_no == None)\
635 .filter(ChangesetComment.f_path == None)
632 .filter(ChangesetComment.f_path == None)
636 if revision:
633 if revision:
637 q = q.filter(ChangesetComment.revision == revision)
634 q = q.filter(ChangesetComment.revision == revision)
638 elif pull_request:
635 elif pull_request:
639 pull_request = self.__get_pull_request(pull_request)
636 pull_request = self.__get_pull_request(pull_request)
640 q = q.filter(ChangesetComment.pull_request == pull_request)
637 q = q.filter(ChangesetComment.pull_request == pull_request)
641 else:
638 else:
642 raise Exception('Please specify commit or pull_request')
639 raise Exception('Please specify commit or pull_request')
643 q = q.order_by(ChangesetComment.created_on)
640 q = q.order_by(ChangesetComment.created_on)
644 return q.all()
641 return q.all()
645
642
646 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
643 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
647 q = self._get_inline_comments_query(repo_id, revision, pull_request)
644 q = self._get_inline_comments_query(repo_id, revision, pull_request)
648 return self._group_comments_by_path_and_line_number(q)
645 return self._group_comments_by_path_and_line_number(q)
649
646
650 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
647 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
651 version=None):
648 version=None):
652 inline_comms = []
649 inline_comms = []
653 for fname, per_line_comments in inline_comments.iteritems():
650 for fname, per_line_comments in inline_comments.iteritems():
654 for lno, comments in per_line_comments.iteritems():
651 for lno, comments in per_line_comments.iteritems():
655 for comm in comments:
652 for comm in comments:
656 if not comm.outdated_at_version(version) and skip_outdated:
653 if not comm.outdated_at_version(version) and skip_outdated:
657 inline_comms.append(comm)
654 inline_comms.append(comm)
658
655
659 return inline_comms
656 return inline_comms
660
657
661 def get_outdated_comments(self, repo_id, pull_request):
658 def get_outdated_comments(self, repo_id, pull_request):
662 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
659 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
663 # of a pull request.
660 # of a pull request.
664 q = self._all_inline_comments_of_pull_request(pull_request)
661 q = self._all_inline_comments_of_pull_request(pull_request)
665 q = q.filter(
662 q = q.filter(
666 ChangesetComment.display_state ==
663 ChangesetComment.display_state ==
667 ChangesetComment.COMMENT_OUTDATED
664 ChangesetComment.COMMENT_OUTDATED
668 ).order_by(ChangesetComment.comment_id.asc())
665 ).order_by(ChangesetComment.comment_id.asc())
669
666
670 return self._group_comments_by_path_and_line_number(q)
667 return self._group_comments_by_path_and_line_number(q)
671
668
672 def _get_inline_comments_query(self, repo_id, revision, pull_request):
669 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
670 # TODO: johbo: Split this into two methods: One for PR and one for
674 # commit.
671 # commit.
675 if revision:
672 if revision:
676 q = Session().query(ChangesetComment).filter(
673 q = Session().query(ChangesetComment).filter(
677 ChangesetComment.repo_id == repo_id,
674 ChangesetComment.repo_id == repo_id,
678 ChangesetComment.line_no != null(),
675 ChangesetComment.line_no != null(),
679 ChangesetComment.f_path != null(),
676 ChangesetComment.f_path != null(),
680 ChangesetComment.revision == revision)
677 ChangesetComment.revision == revision)
681
678
682 elif pull_request:
679 elif pull_request:
683 pull_request = self.__get_pull_request(pull_request)
680 pull_request = self.__get_pull_request(pull_request)
684 if not CommentsModel.use_outdated_comments(pull_request):
681 if not CommentsModel.use_outdated_comments(pull_request):
685 q = self._visible_inline_comments_of_pull_request(pull_request)
682 q = self._visible_inline_comments_of_pull_request(pull_request)
686 else:
683 else:
687 q = self._all_inline_comments_of_pull_request(pull_request)
684 q = self._all_inline_comments_of_pull_request(pull_request)
688
685
689 else:
686 else:
690 raise Exception('Please specify commit or pull_request_id')
687 raise Exception('Please specify commit or pull_request_id')
691 q = q.order_by(ChangesetComment.comment_id.asc())
688 q = q.order_by(ChangesetComment.comment_id.asc())
692 return q
689 return q
693
690
694 def _group_comments_by_path_and_line_number(self, q):
691 def _group_comments_by_path_and_line_number(self, q):
695 comments = q.all()
692 comments = q.all()
696 paths = collections.defaultdict(lambda: collections.defaultdict(list))
693 paths = collections.defaultdict(lambda: collections.defaultdict(list))
697 for co in comments:
694 for co in comments:
698 paths[co.f_path][co.line_no].append(co)
695 paths[co.f_path][co.line_no].append(co)
699 return paths
696 return paths
700
697
701 @classmethod
698 @classmethod
702 def needed_extra_diff_context(cls):
699 def needed_extra_diff_context(cls):
703 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
700 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
704
701
705 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
702 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
706 if not CommentsModel.use_outdated_comments(pull_request):
703 if not CommentsModel.use_outdated_comments(pull_request):
707 return
704 return
708
705
709 comments = self._visible_inline_comments_of_pull_request(pull_request)
706 comments = self._visible_inline_comments_of_pull_request(pull_request)
710 comments_to_outdate = comments.all()
707 comments_to_outdate = comments.all()
711
708
712 for comment in comments_to_outdate:
709 for comment in comments_to_outdate:
713 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
710 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
714
711
715 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
712 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
716 diff_line = _parse_comment_line_number(comment.line_no)
713 diff_line = _parse_comment_line_number(comment.line_no)
717
714
718 try:
715 try:
719 old_context = old_diff_proc.get_context_of_line(
716 old_context = old_diff_proc.get_context_of_line(
720 path=comment.f_path, diff_line=diff_line)
717 path=comment.f_path, diff_line=diff_line)
721 new_context = new_diff_proc.get_context_of_line(
718 new_context = new_diff_proc.get_context_of_line(
722 path=comment.f_path, diff_line=diff_line)
719 path=comment.f_path, diff_line=diff_line)
723 except (diffs.LineNotInDiffException,
720 except (diffs.LineNotInDiffException,
724 diffs.FileNotInDiffException):
721 diffs.FileNotInDiffException):
725 if not comment.draft:
722 if not comment.draft:
726 comment.display_state = ChangesetComment.COMMENT_OUTDATED
723 comment.display_state = ChangesetComment.COMMENT_OUTDATED
727 return
724 return
728
725
729 if old_context == new_context:
726 if old_context == new_context:
730 return
727 return
731
728
732 if self._should_relocate_diff_line(diff_line):
729 if self._should_relocate_diff_line(diff_line):
733 new_diff_lines = new_diff_proc.find_context(
730 new_diff_lines = new_diff_proc.find_context(
734 path=comment.f_path, context=old_context,
731 path=comment.f_path, context=old_context,
735 offset=self.DIFF_CONTEXT_BEFORE)
732 offset=self.DIFF_CONTEXT_BEFORE)
736 if not new_diff_lines and not comment.draft:
733 if not new_diff_lines and not comment.draft:
737 comment.display_state = ChangesetComment.COMMENT_OUTDATED
734 comment.display_state = ChangesetComment.COMMENT_OUTDATED
738 else:
735 else:
739 new_diff_line = self._choose_closest_diff_line(
736 new_diff_line = self._choose_closest_diff_line(
740 diff_line, new_diff_lines)
737 diff_line, new_diff_lines)
741 comment.line_no = _diff_to_comment_line_number(new_diff_line)
738 comment.line_no = _diff_to_comment_line_number(new_diff_line)
742 else:
739 else:
743 if not comment.draft:
740 if not comment.draft:
744 comment.display_state = ChangesetComment.COMMENT_OUTDATED
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
745
742
746 def _should_relocate_diff_line(self, diff_line):
743 def _should_relocate_diff_line(self, diff_line):
747 """
744 """
748 Checks if relocation shall be tried for the given `diff_line`.
745 Checks if relocation shall be tried for the given `diff_line`.
749
746
750 If a comment points into the first lines, then we can have a situation
747 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
748 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
749 we would find the context still and move the comment around. This
753 would be wrong.
750 would be wrong.
754 """
751 """
755 should_relocate = (
752 should_relocate = (
756 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
753 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
757 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
754 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
758 return should_relocate
755 return should_relocate
759
756
760 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
757 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
761 candidate = new_diff_lines[0]
758 candidate = new_diff_lines[0]
762 best_delta = _diff_line_delta(diff_line, candidate)
759 best_delta = _diff_line_delta(diff_line, candidate)
763 for new_diff_line in new_diff_lines[1:]:
760 for new_diff_line in new_diff_lines[1:]:
764 delta = _diff_line_delta(diff_line, new_diff_line)
761 delta = _diff_line_delta(diff_line, new_diff_line)
765 if delta < best_delta:
762 if delta < best_delta:
766 candidate = new_diff_line
763 candidate = new_diff_line
767 best_delta = delta
764 best_delta = delta
768 return candidate
765 return candidate
769
766
770 def _visible_inline_comments_of_pull_request(self, pull_request):
767 def _visible_inline_comments_of_pull_request(self, pull_request):
771 comments = self._all_inline_comments_of_pull_request(pull_request)
768 comments = self._all_inline_comments_of_pull_request(pull_request)
772 comments = comments.filter(
769 comments = comments.filter(
773 coalesce(ChangesetComment.display_state, '') !=
770 coalesce(ChangesetComment.display_state, '') !=
774 ChangesetComment.COMMENT_OUTDATED)
771 ChangesetComment.COMMENT_OUTDATED)
775 return comments
772 return comments
776
773
777 def _all_inline_comments_of_pull_request(self, pull_request):
774 def _all_inline_comments_of_pull_request(self, pull_request):
778 comments = Session().query(ChangesetComment)\
775 comments = Session().query(ChangesetComment)\
779 .filter(ChangesetComment.line_no != None)\
776 .filter(ChangesetComment.line_no != None)\
780 .filter(ChangesetComment.f_path != None)\
777 .filter(ChangesetComment.f_path != None)\
781 .filter(ChangesetComment.pull_request == pull_request)
778 .filter(ChangesetComment.pull_request == pull_request)
782 return comments
779 return comments
783
780
784 def _all_general_comments_of_pull_request(self, pull_request):
781 def _all_general_comments_of_pull_request(self, pull_request):
785 comments = Session().query(ChangesetComment)\
782 comments = Session().query(ChangesetComment)\
786 .filter(ChangesetComment.line_no == None)\
783 .filter(ChangesetComment.line_no == None)\
787 .filter(ChangesetComment.f_path == None)\
784 .filter(ChangesetComment.f_path == None)\
788 .filter(ChangesetComment.pull_request == pull_request)
785 .filter(ChangesetComment.pull_request == pull_request)
789
786
790 return comments
787 return comments
791
788
792 @staticmethod
789 @staticmethod
793 def use_outdated_comments(pull_request):
790 def use_outdated_comments(pull_request):
794 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
791 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
795 settings = settings_model.get_general_settings()
792 settings = settings_model.get_general_settings()
796 return settings.get('rhodecode_use_outdated_comments', False)
793 return settings.get('rhodecode_use_outdated_comments', False)
797
794
798 def trigger_commit_comment_hook(self, repo, user, action, data=None):
795 def trigger_commit_comment_hook(self, repo, user, action, data=None):
799 repo = self._get_repo(repo)
796 repo = self._get_repo(repo)
800 target_scm = repo.scm_instance()
797 target_scm = repo.scm_instance()
801 if action == 'create':
798 if action == 'create':
802 trigger_hook = hooks_utils.trigger_comment_commit_hooks
799 trigger_hook = hooks_utils.trigger_comment_commit_hooks
803 elif action == 'edit':
800 elif action == 'edit':
804 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
801 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
805 else:
802 else:
806 return
803 return
807
804
808 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
805 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
809 repo, action, trigger_hook)
806 repo, action, trigger_hook)
810 trigger_hook(
807 trigger_hook(
811 username=user.username,
808 username=user.username,
812 repo_name=repo.repo_name,
809 repo_name=repo.repo_name,
813 repo_type=target_scm.alias,
810 repo_type=target_scm.alias,
814 repo=repo,
811 repo=repo,
815 data=data)
812 data=data)
816
813
817
814
818 def _parse_comment_line_number(line_no):
815 def _parse_comment_line_number(line_no):
819 """
816 """
820 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
817 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
821 """
818 """
822 old_line = None
819 old_line = None
823 new_line = None
820 new_line = None
824 if line_no.startswith('o'):
821 if line_no.startswith('o'):
825 old_line = int(line_no[1:])
822 old_line = int(line_no[1:])
826 elif line_no.startswith('n'):
823 elif line_no.startswith('n'):
827 new_line = int(line_no[1:])
824 new_line = int(line_no[1:])
828 else:
825 else:
829 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
826 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
830 return diffs.DiffLineNumber(old_line, new_line)
827 return diffs.DiffLineNumber(old_line, new_line)
831
828
832
829
833 def _diff_to_comment_line_number(diff_line):
830 def _diff_to_comment_line_number(diff_line):
834 if diff_line.new is not None:
831 if diff_line.new is not None:
835 return u'n{}'.format(diff_line.new)
832 return u'n{}'.format(diff_line.new)
836 elif diff_line.old is not None:
833 elif diff_line.old is not None:
837 return u'o{}'.format(diff_line.old)
834 return u'o{}'.format(diff_line.old)
838 return u''
835 return u''
839
836
840
837
841 def _diff_line_delta(a, b):
838 def _diff_line_delta(a, b):
842 if None not in (a.new, b.new):
839 if None not in (a.new, b.new):
843 return abs(a.new - b.new)
840 return abs(a.new - b.new)
844 elif None not in (a.old, b.old):
841 elif None not in (a.old, b.old):
845 return abs(a.old - b.old)
842 return abs(a.old - b.old)
846 else:
843 else:
847 raise ValueError(
844 raise ValueError(
848 "Cannot compute delta between {} and {}".format(a, b))
845 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,435 +1,450 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 """
22 """
23 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import premailer
29 import premailer
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.sql.expression import false, true
31 from sqlalchemy.sql.expression import false, true
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
38 from rhodecode.translation import TranslationString
38 from rhodecode.translation import TranslationString
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class NotificationModel(BaseModel):
43 class NotificationModel(BaseModel):
44
44
45 cls = Notification
45 cls = Notification
46
46
47 def __get_notification(self, notification):
47 def __get_notification(self, notification):
48 if isinstance(notification, Notification):
48 if isinstance(notification, Notification):
49 return notification
49 return notification
50 elif isinstance(notification, (int, long)):
50 elif isinstance(notification, (int, long)):
51 return Notification.get(notification)
51 return Notification.get(notification)
52 else:
52 else:
53 if notification:
53 if notification:
54 raise Exception('notification must be int, long or Instance'
54 raise Exception('notification must be int, long or Instance'
55 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
56
56
57 def create(
57 def create(
58 self, created_by, notification_subject, notification_body,
58 self, created_by, notification_subject='', notification_body='',
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 mention_recipients=None, with_email=True, email_kwargs=None):
60 mention_recipients=None, with_email=True, email_kwargs=None):
61 """
61 """
62
62
63 Creates notification of given type
63 Creates notification of given type
64
64
65 :param created_by: int, str or User instance. User who created this
65 :param created_by: int, str or User instance. User who created this
66 notification
66 notification
67 :param notification_subject: subject of notification itself
67 :param notification_subject: subject of notification itself,
68 it will be generated automatically from notification_type if not specified
68 :param notification_body: body of notification text
69 :param notification_body: body of notification text
70 it will be generated automatically from notification_type if not specified
69 :param notification_type: type of notification, based on that we
71 :param notification_type: type of notification, based on that we
70 pick templates
72 pick templates
71
72 :param recipients: list of int, str or User objects, when None
73 :param recipients: list of int, str or User objects, when None
73 is given send to all admins
74 is given send to all admins
74 :param mention_recipients: list of int, str or User objects,
75 :param mention_recipients: list of int, str or User objects,
75 that were mentioned
76 that were mentioned
76 :param with_email: send email with this notification
77 :param with_email: send email with this notification
77 :param email_kwargs: dict with arguments to generate email
78 :param email_kwargs: dict with arguments to generate email
78 """
79 """
79
80
80 from rhodecode.lib.celerylib import tasks, run_task
81 from rhodecode.lib.celerylib import tasks, run_task
81
82
82 if recipients and not getattr(recipients, '__iter__', False):
83 if recipients and not getattr(recipients, '__iter__', False):
83 raise Exception('recipients must be an iterable object')
84 raise Exception('recipients must be an iterable object')
84
85
86 if not (notification_subject and notification_body) and not notification_type:
87 raise ValueError('notification_subject, and notification_body '
88 'cannot be empty when notification_type is not specified')
89
85 created_by_obj = self._get_user(created_by)
90 created_by_obj = self._get_user(created_by)
91
92 if not created_by_obj:
93 raise Exception('unknown user %s' % created_by)
94
86 # default MAIN body if not given
95 # default MAIN body if not given
87 email_kwargs = email_kwargs or {'body': notification_body}
96 email_kwargs = email_kwargs or {'body': notification_body}
88 mention_recipients = mention_recipients or set()
97 mention_recipients = mention_recipients or set()
89
98
90 if not created_by_obj:
91 raise Exception('unknown user %s' % created_by)
92
93 if recipients is None:
99 if recipients is None:
94 # recipients is None means to all admins
100 # recipients is None means to all admins
95 recipients_objs = User.query().filter(User.admin == true()).all()
101 recipients_objs = User.query().filter(User.admin == true()).all()
96 log.debug('sending notifications %s to admins: %s',
102 log.debug('sending notifications %s to admins: %s',
97 notification_type, recipients_objs)
103 notification_type, recipients_objs)
98 else:
104 else:
99 recipients_objs = set()
105 recipients_objs = set()
100 for u in recipients:
106 for u in recipients:
101 obj = self._get_user(u)
107 obj = self._get_user(u)
102 if obj:
108 if obj:
103 recipients_objs.add(obj)
109 recipients_objs.add(obj)
104 else: # we didn't find this user, log the error and carry on
110 else: # we didn't find this user, log the error and carry on
105 log.error('cannot notify unknown user %r', u)
111 log.error('cannot notify unknown user %r', u)
106
112
107 if not recipients_objs:
113 if not recipients_objs:
108 raise Exception('no valid recipients specified')
114 raise Exception('no valid recipients specified')
109
115
110 log.debug('sending notifications %s to %s',
116 log.debug('sending notifications %s to %s',
111 notification_type, recipients_objs)
117 notification_type, recipients_objs)
112
118
113 # add mentioned users into recipients
119 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
120 final_recipients = set(recipients_objs).union(mention_recipients)
115
121
122 (subject, email_body, email_body_plaintext) = \
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
124
125 if not notification_subject:
126 notification_subject = subject
127
128 if not notification_body:
129 notification_body = email_body_plaintext
130
116 notification = Notification.create(
131 notification = Notification.create(
117 created_by=created_by_obj, subject=notification_subject,
132 created_by=created_by_obj, subject=notification_subject,
118 body=notification_body, recipients=final_recipients,
133 body=notification_body, recipients=final_recipients,
119 type_=notification_type
134 type_=notification_type
120 )
135 )
121
136
122 if not with_email: # skip sending email, and just create notification
137 if not with_email: # skip sending email, and just create notification
123 return notification
138 return notification
124
139
125 # don't send email to person who created this comment
140 # don't send email to person who created this comment
126 rec_objs = set(recipients_objs).difference({created_by_obj})
141 rec_objs = set(recipients_objs).difference({created_by_obj})
127
142
128 # now notify all recipients in question
143 # now notify all recipients in question
129
144
130 for recipient in rec_objs.union(mention_recipients):
145 for recipient in rec_objs.union(mention_recipients):
131 # inject current recipient
146 # inject current recipient
132 email_kwargs['recipient'] = recipient
147 email_kwargs['recipient'] = recipient
133 email_kwargs['mention'] = recipient in mention_recipients
148 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
149 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
150 notification_type, **email_kwargs)
136
151
137 extra_headers = None
152 extra_headers = None
138 if 'thread_ids' in email_kwargs:
153 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
154 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
155
141 log.debug('Creating notification email task for user:`%s`', recipient)
156 log.debug('Creating notification email task for user:`%s`', recipient)
142 task = run_task(
157 task = run_task(
143 tasks.send_email, recipient.email, subject,
158 tasks.send_email, recipient.email, subject,
144 email_body_plaintext, email_body, extra_headers=extra_headers)
159 email_body_plaintext, email_body, extra_headers=extra_headers)
145 log.debug('Created email task: %s', task)
160 log.debug('Created email task: %s', task)
146
161
147 return notification
162 return notification
148
163
149 def delete(self, user, notification):
164 def delete(self, user, notification):
150 # we don't want to remove actual notification just the assignment
165 # we don't want to remove actual notification just the assignment
151 try:
166 try:
152 notification = self.__get_notification(notification)
167 notification = self.__get_notification(notification)
153 user = self._get_user(user)
168 user = self._get_user(user)
154 if notification and user:
169 if notification and user:
155 obj = UserNotification.query()\
170 obj = UserNotification.query()\
156 .filter(UserNotification.user == user)\
171 .filter(UserNotification.user == user)\
157 .filter(UserNotification.notification == notification)\
172 .filter(UserNotification.notification == notification)\
158 .one()
173 .one()
159 Session().delete(obj)
174 Session().delete(obj)
160 return True
175 return True
161 except Exception:
176 except Exception:
162 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
163 raise
178 raise
164
179
165 def get_for_user(self, user, filter_=None):
180 def get_for_user(self, user, filter_=None):
166 """
181 """
167 Get mentions for given user, filter them if filter dict is given
182 Get mentions for given user, filter them if filter dict is given
168 """
183 """
169 user = self._get_user(user)
184 user = self._get_user(user)
170
185
171 q = UserNotification.query()\
186 q = UserNotification.query()\
172 .filter(UserNotification.user == user)\
187 .filter(UserNotification.user == user)\
173 .join((
188 .join((
174 Notification, UserNotification.notification_id ==
189 Notification, UserNotification.notification_id ==
175 Notification.notification_id))
190 Notification.notification_id))
176 if filter_ == ['all']:
191 if filter_ == ['all']:
177 q = q # no filter
192 q = q # no filter
178 elif filter_ == ['unread']:
193 elif filter_ == ['unread']:
179 q = q.filter(UserNotification.read == false())
194 q = q.filter(UserNotification.read == false())
180 elif filter_:
195 elif filter_:
181 q = q.filter(Notification.type_.in_(filter_))
196 q = q.filter(Notification.type_.in_(filter_))
182
197
183 return q
198 return q
184
199
185 def mark_read(self, user, notification):
200 def mark_read(self, user, notification):
186 try:
201 try:
187 notification = self.__get_notification(notification)
202 notification = self.__get_notification(notification)
188 user = self._get_user(user)
203 user = self._get_user(user)
189 if notification and user:
204 if notification and user:
190 obj = UserNotification.query()\
205 obj = UserNotification.query()\
191 .filter(UserNotification.user == user)\
206 .filter(UserNotification.user == user)\
192 .filter(UserNotification.notification == notification)\
207 .filter(UserNotification.notification == notification)\
193 .one()
208 .one()
194 obj.read = True
209 obj.read = True
195 Session().add(obj)
210 Session().add(obj)
196 return True
211 return True
197 except Exception:
212 except Exception:
198 log.error(traceback.format_exc())
213 log.error(traceback.format_exc())
199 raise
214 raise
200
215
201 def mark_all_read_for_user(self, user, filter_=None):
216 def mark_all_read_for_user(self, user, filter_=None):
202 user = self._get_user(user)
217 user = self._get_user(user)
203 q = UserNotification.query()\
218 q = UserNotification.query()\
204 .filter(UserNotification.user == user)\
219 .filter(UserNotification.user == user)\
205 .filter(UserNotification.read == false())\
220 .filter(UserNotification.read == false())\
206 .join((
221 .join((
207 Notification, UserNotification.notification_id ==
222 Notification, UserNotification.notification_id ==
208 Notification.notification_id))
223 Notification.notification_id))
209 if filter_ == ['unread']:
224 if filter_ == ['unread']:
210 q = q.filter(UserNotification.read == false())
225 q = q.filter(UserNotification.read == false())
211 elif filter_:
226 elif filter_:
212 q = q.filter(Notification.type_.in_(filter_))
227 q = q.filter(Notification.type_.in_(filter_))
213
228
214 # this is a little inefficient but sqlalchemy doesn't support
229 # this is a little inefficient but sqlalchemy doesn't support
215 # update on joined tables :(
230 # update on joined tables :(
216 for obj in q.all():
231 for obj in q.all():
217 obj.read = True
232 obj.read = True
218 Session().add(obj)
233 Session().add(obj)
219
234
220 def get_unread_cnt_for_user(self, user):
235 def get_unread_cnt_for_user(self, user):
221 user = self._get_user(user)
236 user = self._get_user(user)
222 return UserNotification.query()\
237 return UserNotification.query()\
223 .filter(UserNotification.read == false())\
238 .filter(UserNotification.read == false())\
224 .filter(UserNotification.user == user).count()
239 .filter(UserNotification.user == user).count()
225
240
226 def get_unread_for_user(self, user):
241 def get_unread_for_user(self, user):
227 user = self._get_user(user)
242 user = self._get_user(user)
228 return [x.notification for x in UserNotification.query()
243 return [x.notification for x in UserNotification.query()
229 .filter(UserNotification.read == false())
244 .filter(UserNotification.read == false())
230 .filter(UserNotification.user == user).all()]
245 .filter(UserNotification.user == user).all()]
231
246
232 def get_user_notification(self, user, notification):
247 def get_user_notification(self, user, notification):
233 user = self._get_user(user)
248 user = self._get_user(user)
234 notification = self.__get_notification(notification)
249 notification = self.__get_notification(notification)
235
250
236 return UserNotification.query()\
251 return UserNotification.query()\
237 .filter(UserNotification.notification == notification)\
252 .filter(UserNotification.notification == notification)\
238 .filter(UserNotification.user == user).scalar()
253 .filter(UserNotification.user == user).scalar()
239
254
240 def make_description(self, notification, translate, show_age=True):
255 def make_description(self, notification, translate, show_age=True):
241 """
256 """
242 Creates a human readable description based on properties
257 Creates a human readable description based on properties
243 of notification object
258 of notification object
244 """
259 """
245 _ = translate
260 _ = translate
246 _map = {
261 _map = {
247 notification.TYPE_CHANGESET_COMMENT: [
262 notification.TYPE_CHANGESET_COMMENT: [
248 _('%(user)s commented on commit %(date_or_age)s'),
263 _('%(user)s commented on commit %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
264 _('%(user)s commented on commit at %(date_or_age)s'),
250 ],
265 ],
251 notification.TYPE_MESSAGE: [
266 notification.TYPE_MESSAGE: [
252 _('%(user)s sent message %(date_or_age)s'),
267 _('%(user)s sent message %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
268 _('%(user)s sent message at %(date_or_age)s'),
254 ],
269 ],
255 notification.TYPE_MENTION: [
270 notification.TYPE_MENTION: [
256 _('%(user)s mentioned you %(date_or_age)s'),
271 _('%(user)s mentioned you %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
272 _('%(user)s mentioned you at %(date_or_age)s'),
258 ],
273 ],
259 notification.TYPE_REGISTRATION: [
274 notification.TYPE_REGISTRATION: [
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
275 _('%(user)s registered in RhodeCode %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
276 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
262 ],
277 ],
263 notification.TYPE_PULL_REQUEST: [
278 notification.TYPE_PULL_REQUEST: [
264 _('%(user)s opened new pull request %(date_or_age)s'),
279 _('%(user)s opened new pull request %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
280 _('%(user)s opened new pull request at %(date_or_age)s'),
266 ],
281 ],
267 notification.TYPE_PULL_REQUEST_UPDATE: [
282 notification.TYPE_PULL_REQUEST_UPDATE: [
268 _('%(user)s updated pull request %(date_or_age)s'),
283 _('%(user)s updated pull request %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
284 _('%(user)s updated pull request at %(date_or_age)s'),
270 ],
285 ],
271 notification.TYPE_PULL_REQUEST_COMMENT: [
286 notification.TYPE_PULL_REQUEST_COMMENT: [
272 _('%(user)s commented on pull request %(date_or_age)s'),
287 _('%(user)s commented on pull request %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
288 _('%(user)s commented on pull request at %(date_or_age)s'),
274 ],
289 ],
275 }
290 }
276
291
277 templates = _map[notification.type_]
292 templates = _map[notification.type_]
278
293
279 if show_age:
294 if show_age:
280 template = templates[0]
295 template = templates[0]
281 date_or_age = h.age(notification.created_on)
296 date_or_age = h.age(notification.created_on)
282 if translate:
297 if translate:
283 date_or_age = translate(date_or_age)
298 date_or_age = translate(date_or_age)
284
299
285 if isinstance(date_or_age, TranslationString):
300 if isinstance(date_or_age, TranslationString):
286 date_or_age = date_or_age.interpolate()
301 date_or_age = date_or_age.interpolate()
287
302
288 else:
303 else:
289 template = templates[1]
304 template = templates[1]
290 date_or_age = h.format_date(notification.created_on)
305 date_or_age = h.format_date(notification.created_on)
291
306
292 return template % {
307 return template % {
293 'user': notification.created_by_user.username,
308 'user': notification.created_by_user.username,
294 'date_or_age': date_or_age,
309 'date_or_age': date_or_age,
295 }
310 }
296
311
297
312
298 # Templates for Titles, that could be overwritten by rcextensions
313 # Templates for Titles, that could be overwritten by rcextensions
299 # Title of email for pull-request update
314 # Title of email for pull-request update
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
315 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
301 # Title of email for request for pull request review
316 # Title of email for request for pull request review
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
317 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
303
318
304 # Title of email for general comment on pull request
319 # Title of email for general comment on pull request
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
320 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
306 # Title of email for general comment which includes status change on pull request
321 # Title of email for general comment which includes status change on pull request
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
322 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
308 # Title of email for inline comment on a file in pull request
323 # Title of email for inline comment on a file in pull request
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
324 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
310
325
311 # Title of email for general comment on commit
326 # Title of email for general comment on commit
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
327 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
313 # Title of email for general comment which includes status change on commit
328 # Title of email for general comment which includes status change on commit
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
329 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
315 # Title of email for inline comment on a file in commit
330 # Title of email for inline comment on a file in commit
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
331 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
317
332
318
333
319 class EmailNotificationModel(BaseModel):
334 class EmailNotificationModel(BaseModel):
320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
335 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
336 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
322 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
337 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
323 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
338 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
324 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
339 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
325 TYPE_MAIN = Notification.TYPE_MESSAGE
340 TYPE_MAIN = Notification.TYPE_MESSAGE
326
341
327 TYPE_PASSWORD_RESET = 'password_reset'
342 TYPE_PASSWORD_RESET = 'password_reset'
328 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
343 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
329 TYPE_EMAIL_TEST = 'email_test'
344 TYPE_EMAIL_TEST = 'email_test'
330 TYPE_EMAIL_EXCEPTION = 'exception'
345 TYPE_EMAIL_EXCEPTION = 'exception'
331 TYPE_TEST = 'test'
346 TYPE_TEST = 'test'
332
347
333 email_types = {
348 email_types = {
334 TYPE_MAIN:
349 TYPE_MAIN:
335 'rhodecode:templates/email_templates/main.mako',
350 'rhodecode:templates/email_templates/main.mako',
336 TYPE_TEST:
351 TYPE_TEST:
337 'rhodecode:templates/email_templates/test.mako',
352 'rhodecode:templates/email_templates/test.mako',
338 TYPE_EMAIL_EXCEPTION:
353 TYPE_EMAIL_EXCEPTION:
339 'rhodecode:templates/email_templates/exception_tracker.mako',
354 'rhodecode:templates/email_templates/exception_tracker.mako',
340 TYPE_EMAIL_TEST:
355 TYPE_EMAIL_TEST:
341 'rhodecode:templates/email_templates/email_test.mako',
356 'rhodecode:templates/email_templates/email_test.mako',
342 TYPE_REGISTRATION:
357 TYPE_REGISTRATION:
343 'rhodecode:templates/email_templates/user_registration.mako',
358 'rhodecode:templates/email_templates/user_registration.mako',
344 TYPE_PASSWORD_RESET:
359 TYPE_PASSWORD_RESET:
345 'rhodecode:templates/email_templates/password_reset.mako',
360 'rhodecode:templates/email_templates/password_reset.mako',
346 TYPE_PASSWORD_RESET_CONFIRMATION:
361 TYPE_PASSWORD_RESET_CONFIRMATION:
347 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
362 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
348 TYPE_COMMIT_COMMENT:
363 TYPE_COMMIT_COMMENT:
349 'rhodecode:templates/email_templates/commit_comment.mako',
364 'rhodecode:templates/email_templates/commit_comment.mako',
350 TYPE_PULL_REQUEST:
365 TYPE_PULL_REQUEST:
351 'rhodecode:templates/email_templates/pull_request_review.mako',
366 'rhodecode:templates/email_templates/pull_request_review.mako',
352 TYPE_PULL_REQUEST_COMMENT:
367 TYPE_PULL_REQUEST_COMMENT:
353 'rhodecode:templates/email_templates/pull_request_comment.mako',
368 'rhodecode:templates/email_templates/pull_request_comment.mako',
354 TYPE_PULL_REQUEST_UPDATE:
369 TYPE_PULL_REQUEST_UPDATE:
355 'rhodecode:templates/email_templates/pull_request_update.mako',
370 'rhodecode:templates/email_templates/pull_request_update.mako',
356 }
371 }
357
372
358 premailer_instance = premailer.Premailer(
373 premailer_instance = premailer.Premailer(
359 cssutils_logging_level=logging.ERROR,
374 cssutils_logging_level=logging.ERROR,
360 cssutils_logging_handler=logging.getLogger().handlers[0]
375 cssutils_logging_handler=logging.getLogger().handlers[0]
361 if logging.getLogger().handlers else None,
376 if logging.getLogger().handlers else None,
362 )
377 )
363
378
364 def __init__(self):
379 def __init__(self):
365 """
380 """
366 Example usage::
381 Example usage::
367
382
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
383 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
384 EmailNotificationModel.TYPE_TEST, **email_kwargs)
370
385
371 """
386 """
372 super(EmailNotificationModel, self).__init__()
387 super(EmailNotificationModel, self).__init__()
373 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
388 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
374
389
375 def _update_kwargs_for_render(self, kwargs):
390 def _update_kwargs_for_render(self, kwargs):
376 """
391 """
377 Inject params required for Mako rendering
392 Inject params required for Mako rendering
378
393
379 :param kwargs:
394 :param kwargs:
380 """
395 """
381
396
382 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
397 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
383 kwargs['rhodecode_version'] = rhodecode.__version__
398 kwargs['rhodecode_version'] = rhodecode.__version__
384 instance_url = h.route_url('home')
399 instance_url = h.route_url('home')
385 _kwargs = {
400 _kwargs = {
386 'instance_url': instance_url,
401 'instance_url': instance_url,
387 'whitespace_filter': self.whitespace_filter,
402 'whitespace_filter': self.whitespace_filter,
388 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
403 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
389 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
404 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
390 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
405 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
391 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
406 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
392 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
407 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
393 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
408 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
394 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
409 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
395 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
410 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
396 }
411 }
397 _kwargs.update(kwargs)
412 _kwargs.update(kwargs)
398 return _kwargs
413 return _kwargs
399
414
400 def whitespace_filter(self, text):
415 def whitespace_filter(self, text):
401 return text.replace('\n', '').replace('\t', '')
416 return text.replace('\n', '').replace('\t', '')
402
417
403 def get_renderer(self, type_, request):
418 def get_renderer(self, type_, request):
404 template_name = self.email_types[type_]
419 template_name = self.email_types[type_]
405 return request.get_partial_renderer(template_name)
420 return request.get_partial_renderer(template_name)
406
421
407 def render_email(self, type_, **kwargs):
422 def render_email(self, type_, **kwargs):
408 """
423 """
409 renders template for email, and returns a tuple of
424 renders template for email, and returns a tuple of
410 (subject, email_headers, email_html_body, email_plaintext_body)
425 (subject, email_headers, email_html_body, email_plaintext_body)
411 """
426 """
412 # translator and helpers inject
427 # translator and helpers inject
413 _kwargs = self._update_kwargs_for_render(kwargs)
428 _kwargs = self._update_kwargs_for_render(kwargs)
414 request = get_current_request()
429 request = get_current_request()
415 email_template = self.get_renderer(type_, request=request)
430 email_template = self.get_renderer(type_, request=request)
416
431
417 subject = email_template.render('subject', **_kwargs)
432 subject = email_template.render('subject', **_kwargs)
418
433
419 try:
434 try:
420 body_plaintext = email_template.render('body_plaintext', **_kwargs)
435 body_plaintext = email_template.render('body_plaintext', **_kwargs)
421 except AttributeError:
436 except AttributeError:
422 # it's not defined in template, ok we can skip it
437 # it's not defined in template, ok we can skip it
423 body_plaintext = ''
438 body_plaintext = ''
424
439
425 # render WHOLE template
440 # render WHOLE template
426 body = email_template.render(None, **_kwargs)
441 body = email_template.render(None, **_kwargs)
427
442
428 try:
443 try:
429 # Inline CSS styles and conversion
444 # Inline CSS styles and conversion
430 body = self.premailer_instance.transform(body)
445 body = self.premailer_instance.transform(body)
431 except Exception:
446 except Exception:
432 log.exception('Failed to parse body with premailer')
447 log.exception('Failed to parse body with premailer')
433 pass
448 pass
434
449
435 return subject, body, body_plaintext
450 return subject, body, body_plaintext
@@ -1,2237 +1,2230 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message"]
160 pre_load=["author", "date", "message"]
161 )
161 )
162
162
163 for commit in _compare_data:
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
166 serialized_commit = dict(
167 author=commit.author,
167 author=commit.author,
168 date=commit.date,
168 date=commit.date,
169 message=commit.message,
169 message=commit.message,
170 commit_id=commit.raw_id,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
171 raw_id=commit.raw_id
172 )
172 )
173 commits.append(serialized_commit)
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
174 user = User.get_from_cs_author(serialized_commit['author'])
175 if user and user not in commit_authors:
175 if user and user not in commit_authors:
176 commit_authors.append(user)
176 commit_authors.append(user)
177
177
178 # lines
178 # lines
179 if get_authors:
179 if get_authors:
180 log.debug('Calculating authors of changed files')
180 log.debug('Calculating authors of changed files')
181 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
182
182
183 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
184
184
185 try:
185 try:
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 except Exception:
187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
188 log.exception("Failed to load node with path %s", fname)
189 continue
189 continue
190
190
191 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
192 continue
192 continue
193
193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
195 if node.is_binary:
196 author = node.last_commit.author
196 author = node.last_commit.author
197 email = node.last_commit.author_email
197 email = node.last_commit.author_email
198
198
199 user = User.get_from_cs_author(author)
199 user = User.get_from_cs_author(author)
200 if user:
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
204
205 continue
205 continue
206
206
207 for annotation in node.annotate:
207 for annotation in node.annotate:
208 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
209 if line_no in lines:
209 if line_no in lines:
210 if commit_id not in _commit_cache:
210 if commit_id not in _commit_cache:
211 _commit_cache[commit_id] = get_commit_func()
211 _commit_cache[commit_id] = get_commit_func()
212 commit = _commit_cache[commit_id]
212 commit = _commit_cache[commit_id]
213 author = commit.author
213 author = commit.author
214 email = commit.author_email
214 email = commit.author_email
215 user = User.get_from_cs_author(author)
215 user = User.get_from_cs_author(author)
216 if user:
216 if user:
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
220
220
221 log.debug('Default reviewers processing finished')
221 log.debug('Default reviewers processing finished')
222
222
223 return {
223 return {
224 'commits': commits,
224 'commits': commits,
225 'files': all_files_changes,
225 'files': all_files_changes,
226 'stats': stats,
226 'stats': stats,
227 'ancestor': ancestor_id,
227 'ancestor': ancestor_id,
228 # original authors of modified files
228 # original authors of modified files
229 'original_authors': {
229 'original_authors': {
230 'users': user_counts,
230 'users': user_counts,
231 'authors': author_counts,
231 'authors': author_counts,
232 'emails': email_counts,
232 'emails': email_counts,
233 },
233 },
234 'commit_authors': commit_authors
234 'commit_authors': commit_authors
235 }
235 }
236
236
237
237
238 class PullRequestModel(BaseModel):
238 class PullRequestModel(BaseModel):
239
239
240 cls = PullRequest
240 cls = PullRequest
241
241
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243
243
244 UPDATE_STATUS_MESSAGES = {
244 UPDATE_STATUS_MESSAGES = {
245 UpdateFailureReason.NONE: lazy_ugettext(
245 UpdateFailureReason.NONE: lazy_ugettext(
246 'Pull request update successful.'),
246 'Pull request update successful.'),
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 'Pull request update failed because of an unknown error.'),
248 'Pull request update failed because of an unknown error.'),
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 'No update needed because the source and target have not changed.'),
250 'No update needed because the source and target have not changed.'),
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 'Pull request cannot be updated because the reference type is '
252 'Pull request cannot be updated because the reference type is '
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 'This pull request cannot be updated because the target '
255 'This pull request cannot be updated because the target '
256 'reference is missing.'),
256 'reference is missing.'),
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 'This pull request cannot be updated because the source '
258 'This pull request cannot be updated because the source '
259 'reference is missing.'),
259 'reference is missing.'),
260 }
260 }
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263
263
264 def __get_pull_request(self, pull_request):
264 def __get_pull_request(self, pull_request):
265 return self._get_instance((
265 return self._get_instance((
266 PullRequest, PullRequestVersion), pull_request)
266 PullRequest, PullRequestVersion), pull_request)
267
267
268 def _check_perms(self, perms, pull_request, user, api=False):
268 def _check_perms(self, perms, pull_request, user, api=False):
269 if not api:
269 if not api:
270 return h.HasRepoPermissionAny(*perms)(
270 return h.HasRepoPermissionAny(*perms)(
271 user=user, repo_name=pull_request.target_repo.repo_name)
271 user=user, repo_name=pull_request.target_repo.repo_name)
272 else:
272 else:
273 return h.HasRepoPermissionAnyApi(*perms)(
273 return h.HasRepoPermissionAnyApi(*perms)(
274 user=user, repo_name=pull_request.target_repo.repo_name)
274 user=user, repo_name=pull_request.target_repo.repo_name)
275
275
276 def check_user_read(self, pull_request, user, api=False):
276 def check_user_read(self, pull_request, user, api=False):
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 return self._check_perms(_perms, pull_request, user, api)
278 return self._check_perms(_perms, pull_request, user, api)
279
279
280 def check_user_merge(self, pull_request, user, api=False):
280 def check_user_merge(self, pull_request, user, api=False):
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 return self._check_perms(_perms, pull_request, user, api)
282 return self._check_perms(_perms, pull_request, user, api)
283
283
284 def check_user_update(self, pull_request, user, api=False):
284 def check_user_update(self, pull_request, user, api=False):
285 owner = user.user_id == pull_request.user_id
285 owner = user.user_id == pull_request.user_id
286 return self.check_user_merge(pull_request, user, api) or owner
286 return self.check_user_merge(pull_request, user, api) or owner
287
287
288 def check_user_delete(self, pull_request, user):
288 def check_user_delete(self, pull_request, user):
289 owner = user.user_id == pull_request.user_id
289 owner = user.user_id == pull_request.user_id
290 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
291 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
292
292
293 def is_user_reviewer(self, pull_request, user):
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
294 return user.user_id in [
295 x.user_id for x in
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
297 if x.user
298 ]
298 ]
299
299
300 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
301 return self.check_user_update(pull_request, user, api) \
301 return self.check_user_update(pull_request, user, api) \
302 or self.is_user_reviewer(pull_request, user)
302 or self.is_user_reviewer(pull_request, user)
303
303
304 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
305 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
306 return self.check_user_read(pull_request, user) or owner
306 return self.check_user_read(pull_request, user) or owner
307
307
308 def get(self, pull_request):
308 def get(self, pull_request):
309 return self.__get_pull_request(pull_request)
309 return self.__get_pull_request(pull_request)
310
310
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 statuses=None, opened_by=None, order_by=None,
312 statuses=None, opened_by=None, order_by=None,
313 order_dir='desc', only_created=False):
313 order_dir='desc', only_created=False):
314 repo = None
314 repo = None
315 if repo_name:
315 if repo_name:
316 repo = self._get_repo(repo_name)
316 repo = self._get_repo(repo_name)
317
317
318 q = PullRequest.query()
318 q = PullRequest.query()
319
319
320 if search_q:
320 if search_q:
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User)
323 q = q.filter(or_(
323 q = q.filter(or_(
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
328 ))
328 ))
329
329
330 # source or target
330 # source or target
331 if repo and source:
331 if repo and source:
332 q = q.filter(PullRequest.source_repo == repo)
332 q = q.filter(PullRequest.source_repo == repo)
333 elif repo:
333 elif repo:
334 q = q.filter(PullRequest.target_repo == repo)
334 q = q.filter(PullRequest.target_repo == repo)
335
335
336 # closed,opened
336 # closed,opened
337 if statuses:
337 if statuses:
338 q = q.filter(PullRequest.status.in_(statuses))
338 q = q.filter(PullRequest.status.in_(statuses))
339
339
340 # opened by filter
340 # opened by filter
341 if opened_by:
341 if opened_by:
342 q = q.filter(PullRequest.user_id.in_(opened_by))
342 q = q.filter(PullRequest.user_id.in_(opened_by))
343
343
344 # only get those that are in "created" state
344 # only get those that are in "created" state
345 if only_created:
345 if only_created:
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
352 'title': PullRequest.title,
352 'title': PullRequest.title,
353 'updated_on_raw': PullRequest.updated_on,
353 'updated_on_raw': PullRequest.updated_on,
354 'target_repo': PullRequest.target_repo_id
354 'target_repo': PullRequest.target_repo_id
355 }
355 }
356 if order_dir == 'asc':
356 if order_dir == 'asc':
357 q = q.order_by(order_map[order_by].asc())
357 q = q.order_by(order_map[order_by].asc())
358 else:
358 else:
359 q = q.order_by(order_map[order_by].desc())
359 q = q.order_by(order_map[order_by].desc())
360
360
361 return q
361 return q
362
362
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 opened_by=None):
364 opened_by=None):
365 """
365 """
366 Count the number of pull requests for a specific repository.
366 Count the number of pull requests for a specific repository.
367
367
368 :param repo_name: target or source repo
368 :param repo_name: target or source repo
369 :param search_q: filter by text
369 :param search_q: filter by text
370 :param source: boolean flag to specify if repo_name refers to source
370 :param source: boolean flag to specify if repo_name refers to source
371 :param statuses: list of pull request statuses
371 :param statuses: list of pull request statuses
372 :param opened_by: author user of the pull request
372 :param opened_by: author user of the pull request
373 :returns: int number of pull requests
373 :returns: int number of pull requests
374 """
374 """
375 q = self._prepare_get_all_query(
375 q = self._prepare_get_all_query(
376 repo_name, search_q=search_q, source=source, statuses=statuses,
376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 opened_by=opened_by)
377 opened_by=opened_by)
378
378
379 return q.count()
379 return q.count()
380
380
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 """
383 """
384 Get all pull requests for a specific repository.
384 Get all pull requests for a specific repository.
385
385
386 :param repo_name: target or source repo
386 :param repo_name: target or source repo
387 :param search_q: filter by text
387 :param search_q: filter by text
388 :param source: boolean flag to specify if repo_name refers to source
388 :param source: boolean flag to specify if repo_name refers to source
389 :param statuses: list of pull request statuses
389 :param statuses: list of pull request statuses
390 :param opened_by: author user of the pull request
390 :param opened_by: author user of the pull request
391 :param offset: pagination offset
391 :param offset: pagination offset
392 :param length: length of returned list
392 :param length: length of returned list
393 :param order_by: order of the returned list
393 :param order_by: order of the returned list
394 :param order_dir: 'asc' or 'desc' ordering direction
394 :param order_dir: 'asc' or 'desc' ordering direction
395 :returns: list of pull requests
395 :returns: list of pull requests
396 """
396 """
397 q = self._prepare_get_all_query(
397 q = self._prepare_get_all_query(
398 repo_name, search_q=search_q, source=source, statuses=statuses,
398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
409 opened_by=None):
409 opened_by=None):
410 """
410 """
411 Count the number of pull requests for a specific repository that are
411 Count the number of pull requests for a specific repository that are
412 awaiting review.
412 awaiting review.
413
413
414 :param repo_name: target or source repo
414 :param repo_name: target or source repo
415 :param search_q: filter by text
415 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
416 :param source: boolean flag to specify if repo_name refers to source
417 :param statuses: list of pull request statuses
417 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
418 :param opened_by: author user of the pull request
419 :returns: int number of pull requests
419 :returns: int number of pull requests
420 """
420 """
421 pull_requests = self.get_awaiting_review(
421 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
423
423
424 return len(pull_requests)
424 return len(pull_requests)
425
425
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
427 opened_by=None, offset=0, length=None,
427 opened_by=None, offset=0, length=None,
428 order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
429 """
429 """
430 Get all pull requests for a specific repository that are awaiting
430 Get all pull requests for a specific repository that are awaiting
431 review.
431 review.
432
432
433 :param repo_name: target or source repo
433 :param repo_name: target or source repo
434 :param search_q: filter by text
434 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
435 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
436 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
437 :param opened_by: author user of the pull request
438 :param offset: pagination offset
438 :param offset: pagination offset
439 :param length: length of returned list
439 :param length: length of returned list
440 :param order_by: order of the returned list
440 :param order_by: order of the returned list
441 :param order_dir: 'asc' or 'desc' ordering direction
441 :param order_dir: 'asc' or 'desc' ordering direction
442 :returns: list of pull requests
442 :returns: list of pull requests
443 """
443 """
444 pull_requests = self.get_all(
444 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
445 repo_name, search_q=search_q, source=source, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
447
447
448 _filtered_pull_requests = []
448 _filtered_pull_requests = []
449 for pr in pull_requests:
449 for pr in pull_requests:
450 status = pr.calculated_review_status()
450 status = pr.calculated_review_status()
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 _filtered_pull_requests.append(pr)
453 _filtered_pull_requests.append(pr)
454 if length:
454 if length:
455 return _filtered_pull_requests[offset:offset+length]
455 return _filtered_pull_requests[offset:offset+length]
456 else:
456 else:
457 return _filtered_pull_requests
457 return _filtered_pull_requests
458
458
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
460 opened_by=None, user_id=None):
460 opened_by=None, user_id=None):
461 """
461 """
462 Count the number of pull requests for a specific repository that are
462 Count the number of pull requests for a specific repository that are
463 awaiting review from a specific user.
463 awaiting review from a specific user.
464
464
465 :param repo_name: target or source repo
465 :param repo_name: target or source repo
466 :param search_q: filter by text
466 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
467 :param source: boolean flag to specify if repo_name refers to source
468 :param statuses: list of pull request statuses
468 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
470 :param user_id: reviewer user of the pull request
471 :returns: int number of pull requests
471 :returns: int number of pull requests
472 """
472 """
473 pull_requests = self.get_awaiting_my_review(
473 pull_requests = self.get_awaiting_my_review(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
474 repo_name, search_q=search_q, source=source, statuses=statuses,
475 opened_by=opened_by, user_id=user_id)
475 opened_by=opened_by, user_id=user_id)
476
476
477 return len(pull_requests)
477 return len(pull_requests)
478
478
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
481 length=None, order_by=None, order_dir='desc'):
482 """
482 """
483 Get all pull requests for a specific repository that are awaiting
483 Get all pull requests for a specific repository that are awaiting
484 review from a specific user.
484 review from a specific user.
485
485
486 :param repo_name: target or source repo
486 :param repo_name: target or source repo
487 :param search_q: filter by text
487 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
488 :param source: boolean flag to specify if repo_name refers to source
489 :param statuses: list of pull request statuses
489 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
491 :param user_id: reviewer user of the pull request
492 :param offset: pagination offset
492 :param offset: pagination offset
493 :param length: length of returned list
493 :param length: length of returned list
494 :param order_by: order of the returned list
494 :param order_by: order of the returned list
495 :param order_dir: 'asc' or 'desc' ordering direction
495 :param order_dir: 'asc' or 'desc' ordering direction
496 :returns: list of pull requests
496 :returns: list of pull requests
497 """
497 """
498 pull_requests = self.get_all(
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501
501
502 _my = PullRequestModel().get_not_reviewed(user_id)
502 _my = PullRequestModel().get_not_reviewed(user_id)
503 my_participation = []
503 my_participation = []
504 for pr in pull_requests:
504 for pr in pull_requests:
505 if pr in _my:
505 if pr in _my:
506 my_participation.append(pr)
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
507 _filtered_pull_requests = my_participation
508 if length:
508 if length:
509 return _filtered_pull_requests[offset:offset+length]
509 return _filtered_pull_requests[offset:offset+length]
510 else:
510 else:
511 return _filtered_pull_requests
511 return _filtered_pull_requests
512
512
513 def get_not_reviewed(self, user_id):
513 def get_not_reviewed(self, user_id):
514 return [
514 return [
515 x.pull_request for x in PullRequestReviewers.query().filter(
515 x.pull_request for x in PullRequestReviewers.query().filter(
516 PullRequestReviewers.user_id == user_id).all()
516 PullRequestReviewers.user_id == user_id).all()
517 ]
517 ]
518
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 order_by=None, order_dir='desc'):
520 order_by=None, order_dir='desc'):
521 q = PullRequest.query()
521 q = PullRequest.query()
522 if user_id:
522 if user_id:
523 reviewers_subquery = Session().query(
523 reviewers_subquery = Session().query(
524 PullRequestReviewers.pull_request_id).filter(
524 PullRequestReviewers.pull_request_id).filter(
525 PullRequestReviewers.user_id == user_id).subquery()
525 PullRequestReviewers.user_id == user_id).subquery()
526 user_filter = or_(
526 user_filter = or_(
527 PullRequest.user_id == user_id,
527 PullRequest.user_id == user_id,
528 PullRequest.pull_request_id.in_(reviewers_subquery)
528 PullRequest.pull_request_id.in_(reviewers_subquery)
529 )
529 )
530 q = PullRequest.query().filter(user_filter)
530 q = PullRequest.query().filter(user_filter)
531
531
532 # closed,opened
532 # closed,opened
533 if statuses:
533 if statuses:
534 q = q.filter(PullRequest.status.in_(statuses))
534 q = q.filter(PullRequest.status.in_(statuses))
535
535
536 if query:
536 if query:
537 like_expression = u'%{}%'.format(safe_unicode(query))
537 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
538 q = q.join(User)
539 q = q.filter(or_(
539 q = q.filter(or_(
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 User.username.ilike(like_expression),
541 User.username.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
544 ))
544 ))
545 if order_by:
545 if order_by:
546 order_map = {
546 order_map = {
547 'name_raw': PullRequest.pull_request_id,
547 'name_raw': PullRequest.pull_request_id,
548 'title': PullRequest.title,
548 'title': PullRequest.title,
549 'updated_on_raw': PullRequest.updated_on,
549 'updated_on_raw': PullRequest.updated_on,
550 'target_repo': PullRequest.target_repo_id
550 'target_repo': PullRequest.target_repo_id
551 }
551 }
552 if order_dir == 'asc':
552 if order_dir == 'asc':
553 q = q.order_by(order_map[order_by].asc())
553 q = q.order_by(order_map[order_by].asc())
554 else:
554 else:
555 q = q.order_by(order_map[order_by].desc())
555 q = q.order_by(order_map[order_by].desc())
556
556
557 return q
557 return q
558
558
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
561 return q.count()
561 return q.count()
562
562
563 def get_im_participating_in(
563 def get_im_participating_in(
564 self, user_id=None, statuses=None, query='', offset=0,
564 self, user_id=None, statuses=None, query='', offset=0,
565 length=None, order_by=None, order_dir='desc'):
565 length=None, order_by=None, order_dir='desc'):
566 """
566 """
567 Get all Pull requests that i'm participating in, or i have opened
567 Get all Pull requests that i'm participating in, or i have opened
568 """
568 """
569
569
570 q = self._prepare_participating_query(
570 q = self._prepare_participating_query(
571 user_id, statuses=statuses, query=query, order_by=order_by,
571 user_id, statuses=statuses, query=query, order_by=order_by,
572 order_dir=order_dir)
572 order_dir=order_dir)
573
573
574 if length:
574 if length:
575 pull_requests = q.limit(length).offset(offset).all()
575 pull_requests = q.limit(length).offset(offset).all()
576 else:
576 else:
577 pull_requests = q.all()
577 pull_requests = q.all()
578
578
579 return pull_requests
579 return pull_requests
580
580
581 def get_versions(self, pull_request):
581 def get_versions(self, pull_request):
582 """
582 """
583 returns version of pull request sorted by ID descending
583 returns version of pull request sorted by ID descending
584 """
584 """
585 return PullRequestVersion.query()\
585 return PullRequestVersion.query()\
586 .filter(PullRequestVersion.pull_request == pull_request)\
586 .filter(PullRequestVersion.pull_request == pull_request)\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 .all()
588 .all()
589
589
590 def get_pr_version(self, pull_request_id, version=None):
590 def get_pr_version(self, pull_request_id, version=None):
591 at_version = None
591 at_version = None
592
592
593 if version and version == 'latest':
593 if version and version == 'latest':
594 pull_request_ver = PullRequest.get(pull_request_id)
594 pull_request_ver = PullRequest.get(pull_request_id)
595 pull_request_obj = pull_request_ver
595 pull_request_obj = pull_request_ver
596 _org_pull_request_obj = pull_request_obj
596 _org_pull_request_obj = pull_request_obj
597 at_version = 'latest'
597 at_version = 'latest'
598 elif version:
598 elif version:
599 pull_request_ver = PullRequestVersion.get_or_404(version)
599 pull_request_ver = PullRequestVersion.get_or_404(version)
600 pull_request_obj = pull_request_ver
600 pull_request_obj = pull_request_ver
601 _org_pull_request_obj = pull_request_ver.pull_request
601 _org_pull_request_obj = pull_request_ver.pull_request
602 at_version = pull_request_ver.pull_request_version_id
602 at_version = pull_request_ver.pull_request_version_id
603 else:
603 else:
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 pull_request_id)
605 pull_request_id)
606
606
607 pull_request_display_obj = PullRequest.get_pr_display_object(
607 pull_request_display_obj = PullRequest.get_pr_display_object(
608 pull_request_obj, _org_pull_request_obj)
608 pull_request_obj, _org_pull_request_obj)
609
609
610 return _org_pull_request_obj, pull_request_obj, \
610 return _org_pull_request_obj, pull_request_obj, \
611 pull_request_display_obj, at_version
611 pull_request_display_obj, at_version
612
612
613 def create(self, created_by, source_repo, source_ref, target_repo,
613 def create(self, created_by, source_repo, source_ref, target_repo,
614 target_ref, revisions, reviewers, observers, title, description=None,
614 target_ref, revisions, reviewers, observers, title, description=None,
615 common_ancestor_id=None,
615 common_ancestor_id=None,
616 description_renderer=None,
616 description_renderer=None,
617 reviewer_data=None, translator=None, auth_user=None):
617 reviewer_data=None, translator=None, auth_user=None):
618 translator = translator or get_current_request().translate
618 translator = translator or get_current_request().translate
619
619
620 created_by_user = self._get_user(created_by)
620 created_by_user = self._get_user(created_by)
621 auth_user = auth_user or created_by_user.AuthUser()
621 auth_user = auth_user or created_by_user.AuthUser()
622 source_repo = self._get_repo(source_repo)
622 source_repo = self._get_repo(source_repo)
623 target_repo = self._get_repo(target_repo)
623 target_repo = self._get_repo(target_repo)
624
624
625 pull_request = PullRequest()
625 pull_request = PullRequest()
626 pull_request.source_repo = source_repo
626 pull_request.source_repo = source_repo
627 pull_request.source_ref = source_ref
627 pull_request.source_ref = source_ref
628 pull_request.target_repo = target_repo
628 pull_request.target_repo = target_repo
629 pull_request.target_ref = target_ref
629 pull_request.target_ref = target_ref
630 pull_request.revisions = revisions
630 pull_request.revisions = revisions
631 pull_request.title = title
631 pull_request.title = title
632 pull_request.description = description
632 pull_request.description = description
633 pull_request.description_renderer = description_renderer
633 pull_request.description_renderer = description_renderer
634 pull_request.author = created_by_user
634 pull_request.author = created_by_user
635 pull_request.reviewer_data = reviewer_data
635 pull_request.reviewer_data = reviewer_data
636 pull_request.pull_request_state = pull_request.STATE_CREATING
636 pull_request.pull_request_state = pull_request.STATE_CREATING
637 pull_request.common_ancestor_id = common_ancestor_id
637 pull_request.common_ancestor_id = common_ancestor_id
638
638
639 Session().add(pull_request)
639 Session().add(pull_request)
640 Session().flush()
640 Session().flush()
641
641
642 reviewer_ids = set()
642 reviewer_ids = set()
643 # members / reviewers
643 # members / reviewers
644 for reviewer_object in reviewers:
644 for reviewer_object in reviewers:
645 user_id, reasons, mandatory, role, rules = reviewer_object
645 user_id, reasons, mandatory, role, rules = reviewer_object
646 user = self._get_user(user_id)
646 user = self._get_user(user_id)
647
647
648 # skip duplicates
648 # skip duplicates
649 if user.user_id in reviewer_ids:
649 if user.user_id in reviewer_ids:
650 continue
650 continue
651
651
652 reviewer_ids.add(user.user_id)
652 reviewer_ids.add(user.user_id)
653
653
654 reviewer = PullRequestReviewers()
654 reviewer = PullRequestReviewers()
655 reviewer.user = user
655 reviewer.user = user
656 reviewer.pull_request = pull_request
656 reviewer.pull_request = pull_request
657 reviewer.reasons = reasons
657 reviewer.reasons = reasons
658 reviewer.mandatory = mandatory
658 reviewer.mandatory = mandatory
659 reviewer.role = role
659 reviewer.role = role
660
660
661 # NOTE(marcink): pick only first rule for now
661 # NOTE(marcink): pick only first rule for now
662 rule_id = list(rules)[0] if rules else None
662 rule_id = list(rules)[0] if rules else None
663 rule = RepoReviewRule.get(rule_id) if rule_id else None
663 rule = RepoReviewRule.get(rule_id) if rule_id else None
664 if rule:
664 if rule:
665 review_group = rule.user_group_vote_rule(user_id)
665 review_group = rule.user_group_vote_rule(user_id)
666 # we check if this particular reviewer is member of a voting group
666 # we check if this particular reviewer is member of a voting group
667 if review_group:
667 if review_group:
668 # NOTE(marcink):
668 # NOTE(marcink):
669 # can be that user is member of more but we pick the first same,
669 # can be that user is member of more but we pick the first same,
670 # same as default reviewers algo
670 # same as default reviewers algo
671 review_group = review_group[0]
671 review_group = review_group[0]
672
672
673 rule_data = {
673 rule_data = {
674 'rule_name':
674 'rule_name':
675 rule.review_rule_name,
675 rule.review_rule_name,
676 'rule_user_group_entry_id':
676 'rule_user_group_entry_id':
677 review_group.repo_review_rule_users_group_id,
677 review_group.repo_review_rule_users_group_id,
678 'rule_user_group_name':
678 'rule_user_group_name':
679 review_group.users_group.users_group_name,
679 review_group.users_group.users_group_name,
680 'rule_user_group_members':
680 'rule_user_group_members':
681 [x.user.username for x in review_group.users_group.members],
681 [x.user.username for x in review_group.users_group.members],
682 'rule_user_group_members_id':
682 'rule_user_group_members_id':
683 [x.user.user_id for x in review_group.users_group.members],
683 [x.user.user_id for x in review_group.users_group.members],
684 }
684 }
685 # e.g {'vote_rule': -1, 'mandatory': True}
685 # e.g {'vote_rule': -1, 'mandatory': True}
686 rule_data.update(review_group.rule_data())
686 rule_data.update(review_group.rule_data())
687
687
688 reviewer.rule_data = rule_data
688 reviewer.rule_data = rule_data
689
689
690 Session().add(reviewer)
690 Session().add(reviewer)
691 Session().flush()
691 Session().flush()
692
692
693 for observer_object in observers:
693 for observer_object in observers:
694 user_id, reasons, mandatory, role, rules = observer_object
694 user_id, reasons, mandatory, role, rules = observer_object
695 user = self._get_user(user_id)
695 user = self._get_user(user_id)
696
696
697 # skip duplicates from reviewers
697 # skip duplicates from reviewers
698 if user.user_id in reviewer_ids:
698 if user.user_id in reviewer_ids:
699 continue
699 continue
700
700
701 #reviewer_ids.add(user.user_id)
701 #reviewer_ids.add(user.user_id)
702
702
703 observer = PullRequestReviewers()
703 observer = PullRequestReviewers()
704 observer.user = user
704 observer.user = user
705 observer.pull_request = pull_request
705 observer.pull_request = pull_request
706 observer.reasons = reasons
706 observer.reasons = reasons
707 observer.mandatory = mandatory
707 observer.mandatory = mandatory
708 observer.role = role
708 observer.role = role
709
709
710 # NOTE(marcink): pick only first rule for now
710 # NOTE(marcink): pick only first rule for now
711 rule_id = list(rules)[0] if rules else None
711 rule_id = list(rules)[0] if rules else None
712 rule = RepoReviewRule.get(rule_id) if rule_id else None
712 rule = RepoReviewRule.get(rule_id) if rule_id else None
713 if rule:
713 if rule:
714 # TODO(marcink): do we need this for observers ??
714 # TODO(marcink): do we need this for observers ??
715 pass
715 pass
716
716
717 Session().add(observer)
717 Session().add(observer)
718 Session().flush()
718 Session().flush()
719
719
720 # Set approval status to "Under Review" for all commits which are
720 # Set approval status to "Under Review" for all commits which are
721 # part of this pull request.
721 # part of this pull request.
722 ChangesetStatusModel().set_status(
722 ChangesetStatusModel().set_status(
723 repo=target_repo,
723 repo=target_repo,
724 status=ChangesetStatus.STATUS_UNDER_REVIEW,
724 status=ChangesetStatus.STATUS_UNDER_REVIEW,
725 user=created_by_user,
725 user=created_by_user,
726 pull_request=pull_request
726 pull_request=pull_request
727 )
727 )
728 # we commit early at this point. This has to do with a fact
728 # we commit early at this point. This has to do with a fact
729 # that before queries do some row-locking. And because of that
729 # that before queries do some row-locking. And because of that
730 # we need to commit and finish transaction before below validate call
730 # we need to commit and finish transaction before below validate call
731 # that for large repos could be long resulting in long row locks
731 # that for large repos could be long resulting in long row locks
732 Session().commit()
732 Session().commit()
733
733
734 # prepare workspace, and run initial merge simulation. Set state during that
734 # prepare workspace, and run initial merge simulation. Set state during that
735 # operation
735 # operation
736 pull_request = PullRequest.get(pull_request.pull_request_id)
736 pull_request = PullRequest.get(pull_request.pull_request_id)
737
737
738 # set as merging, for merge simulation, and if finished to created so we mark
738 # set as merging, for merge simulation, and if finished to created so we mark
739 # simulation is working fine
739 # simulation is working fine
740 with pull_request.set_state(PullRequest.STATE_MERGING,
740 with pull_request.set_state(PullRequest.STATE_MERGING,
741 final_state=PullRequest.STATE_CREATED) as state_obj:
741 final_state=PullRequest.STATE_CREATED) as state_obj:
742 MergeCheck.validate(
742 MergeCheck.validate(
743 pull_request, auth_user=auth_user, translator=translator)
743 pull_request, auth_user=auth_user, translator=translator)
744
744
745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
747
747
748 creation_data = pull_request.get_api_data(with_merge_state=False)
748 creation_data = pull_request.get_api_data(with_merge_state=False)
749 self._log_audit_action(
749 self._log_audit_action(
750 'repo.pull_request.create', {'data': creation_data},
750 'repo.pull_request.create', {'data': creation_data},
751 auth_user, pull_request)
751 auth_user, pull_request)
752
752
753 return pull_request
753 return pull_request
754
754
755 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
755 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
756 pull_request = self.__get_pull_request(pull_request)
756 pull_request = self.__get_pull_request(pull_request)
757 target_scm = pull_request.target_repo.scm_instance()
757 target_scm = pull_request.target_repo.scm_instance()
758 if action == 'create':
758 if action == 'create':
759 trigger_hook = hooks_utils.trigger_create_pull_request_hook
759 trigger_hook = hooks_utils.trigger_create_pull_request_hook
760 elif action == 'merge':
760 elif action == 'merge':
761 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
761 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
762 elif action == 'close':
762 elif action == 'close':
763 trigger_hook = hooks_utils.trigger_close_pull_request_hook
763 trigger_hook = hooks_utils.trigger_close_pull_request_hook
764 elif action == 'review_status_change':
764 elif action == 'review_status_change':
765 trigger_hook = hooks_utils.trigger_review_pull_request_hook
765 trigger_hook = hooks_utils.trigger_review_pull_request_hook
766 elif action == 'update':
766 elif action == 'update':
767 trigger_hook = hooks_utils.trigger_update_pull_request_hook
767 trigger_hook = hooks_utils.trigger_update_pull_request_hook
768 elif action == 'comment':
768 elif action == 'comment':
769 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
769 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
770 elif action == 'comment_edit':
770 elif action == 'comment_edit':
771 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
771 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
772 else:
772 else:
773 return
773 return
774
774
775 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
775 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
776 pull_request, action, trigger_hook)
776 pull_request, action, trigger_hook)
777 trigger_hook(
777 trigger_hook(
778 username=user.username,
778 username=user.username,
779 repo_name=pull_request.target_repo.repo_name,
779 repo_name=pull_request.target_repo.repo_name,
780 repo_type=target_scm.alias,
780 repo_type=target_scm.alias,
781 pull_request=pull_request,
781 pull_request=pull_request,
782 data=data)
782 data=data)
783
783
784 def _get_commit_ids(self, pull_request):
784 def _get_commit_ids(self, pull_request):
785 """
785 """
786 Return the commit ids of the merged pull request.
786 Return the commit ids of the merged pull request.
787
787
788 This method is not dealing correctly yet with the lack of autoupdates
788 This method is not dealing correctly yet with the lack of autoupdates
789 nor with the implicit target updates.
789 nor with the implicit target updates.
790 For example: if a commit in the source repo is already in the target it
790 For example: if a commit in the source repo is already in the target it
791 will be reported anyways.
791 will be reported anyways.
792 """
792 """
793 merge_rev = pull_request.merge_rev
793 merge_rev = pull_request.merge_rev
794 if merge_rev is None:
794 if merge_rev is None:
795 raise ValueError('This pull request was not merged yet')
795 raise ValueError('This pull request was not merged yet')
796
796
797 commit_ids = list(pull_request.revisions)
797 commit_ids = list(pull_request.revisions)
798 if merge_rev not in commit_ids:
798 if merge_rev not in commit_ids:
799 commit_ids.append(merge_rev)
799 commit_ids.append(merge_rev)
800
800
801 return commit_ids
801 return commit_ids
802
802
803 def merge_repo(self, pull_request, user, extras):
803 def merge_repo(self, pull_request, user, extras):
804 log.debug("Merging pull request %s", pull_request.pull_request_id)
804 log.debug("Merging pull request %s", pull_request.pull_request_id)
805 extras['user_agent'] = 'internal-merge'
805 extras['user_agent'] = 'internal-merge'
806 merge_state = self._merge_pull_request(pull_request, user, extras)
806 merge_state = self._merge_pull_request(pull_request, user, extras)
807 if merge_state.executed:
807 if merge_state.executed:
808 log.debug("Merge was successful, updating the pull request comments.")
808 log.debug("Merge was successful, updating the pull request comments.")
809 self._comment_and_close_pr(pull_request, user, merge_state)
809 self._comment_and_close_pr(pull_request, user, merge_state)
810
810
811 self._log_audit_action(
811 self._log_audit_action(
812 'repo.pull_request.merge',
812 'repo.pull_request.merge',
813 {'merge_state': merge_state.__dict__},
813 {'merge_state': merge_state.__dict__},
814 user, pull_request)
814 user, pull_request)
815
815
816 else:
816 else:
817 log.warn("Merge failed, not updating the pull request.")
817 log.warn("Merge failed, not updating the pull request.")
818 return merge_state
818 return merge_state
819
819
820 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
820 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
821 target_vcs = pull_request.target_repo.scm_instance()
821 target_vcs = pull_request.target_repo.scm_instance()
822 source_vcs = pull_request.source_repo.scm_instance()
822 source_vcs = pull_request.source_repo.scm_instance()
823
823
824 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
824 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
825 pr_id=pull_request.pull_request_id,
825 pr_id=pull_request.pull_request_id,
826 pr_title=pull_request.title,
826 pr_title=pull_request.title,
827 source_repo=source_vcs.name,
827 source_repo=source_vcs.name,
828 source_ref_name=pull_request.source_ref_parts.name,
828 source_ref_name=pull_request.source_ref_parts.name,
829 target_repo=target_vcs.name,
829 target_repo=target_vcs.name,
830 target_ref_name=pull_request.target_ref_parts.name,
830 target_ref_name=pull_request.target_ref_parts.name,
831 )
831 )
832
832
833 workspace_id = self._workspace_id(pull_request)
833 workspace_id = self._workspace_id(pull_request)
834 repo_id = pull_request.target_repo.repo_id
834 repo_id = pull_request.target_repo.repo_id
835 use_rebase = self._use_rebase_for_merging(pull_request)
835 use_rebase = self._use_rebase_for_merging(pull_request)
836 close_branch = self._close_branch_before_merging(pull_request)
836 close_branch = self._close_branch_before_merging(pull_request)
837 user_name = self._user_name_for_merging(pull_request, user)
837 user_name = self._user_name_for_merging(pull_request, user)
838
838
839 target_ref = self._refresh_reference(
839 target_ref = self._refresh_reference(
840 pull_request.target_ref_parts, target_vcs)
840 pull_request.target_ref_parts, target_vcs)
841
841
842 callback_daemon, extras = prepare_callback_daemon(
842 callback_daemon, extras = prepare_callback_daemon(
843 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
843 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
844 host=vcs_settings.HOOKS_HOST,
844 host=vcs_settings.HOOKS_HOST,
845 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
845 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
846
846
847 with callback_daemon:
847 with callback_daemon:
848 # TODO: johbo: Implement a clean way to run a config_override
848 # TODO: johbo: Implement a clean way to run a config_override
849 # for a single call.
849 # for a single call.
850 target_vcs.config.set(
850 target_vcs.config.set(
851 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
851 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
852
852
853 merge_state = target_vcs.merge(
853 merge_state = target_vcs.merge(
854 repo_id, workspace_id, target_ref, source_vcs,
854 repo_id, workspace_id, target_ref, source_vcs,
855 pull_request.source_ref_parts,
855 pull_request.source_ref_parts,
856 user_name=user_name, user_email=user.email,
856 user_name=user_name, user_email=user.email,
857 message=message, use_rebase=use_rebase,
857 message=message, use_rebase=use_rebase,
858 close_branch=close_branch)
858 close_branch=close_branch)
859 return merge_state
859 return merge_state
860
860
861 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
861 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
862 pull_request.merge_rev = merge_state.merge_ref.commit_id
862 pull_request.merge_rev = merge_state.merge_ref.commit_id
863 pull_request.updated_on = datetime.datetime.now()
863 pull_request.updated_on = datetime.datetime.now()
864 close_msg = close_msg or 'Pull request merged and closed'
864 close_msg = close_msg or 'Pull request merged and closed'
865
865
866 CommentsModel().create(
866 CommentsModel().create(
867 text=safe_unicode(close_msg),
867 text=safe_unicode(close_msg),
868 repo=pull_request.target_repo.repo_id,
868 repo=pull_request.target_repo.repo_id,
869 user=user.user_id,
869 user=user.user_id,
870 pull_request=pull_request.pull_request_id,
870 pull_request=pull_request.pull_request_id,
871 f_path=None,
871 f_path=None,
872 line_no=None,
872 line_no=None,
873 closing_pr=True
873 closing_pr=True
874 )
874 )
875
875
876 Session().add(pull_request)
876 Session().add(pull_request)
877 Session().flush()
877 Session().flush()
878 # TODO: paris: replace invalidation with less radical solution
878 # TODO: paris: replace invalidation with less radical solution
879 ScmModel().mark_for_invalidation(
879 ScmModel().mark_for_invalidation(
880 pull_request.target_repo.repo_name)
880 pull_request.target_repo.repo_name)
881 self.trigger_pull_request_hook(pull_request, user, 'merge')
881 self.trigger_pull_request_hook(pull_request, user, 'merge')
882
882
883 def has_valid_update_type(self, pull_request):
883 def has_valid_update_type(self, pull_request):
884 source_ref_type = pull_request.source_ref_parts.type
884 source_ref_type = pull_request.source_ref_parts.type
885 return source_ref_type in self.REF_TYPES
885 return source_ref_type in self.REF_TYPES
886
886
887 def get_flow_commits(self, pull_request):
887 def get_flow_commits(self, pull_request):
888
888
889 # source repo
889 # source repo
890 source_ref_name = pull_request.source_ref_parts.name
890 source_ref_name = pull_request.source_ref_parts.name
891 source_ref_type = pull_request.source_ref_parts.type
891 source_ref_type = pull_request.source_ref_parts.type
892 source_ref_id = pull_request.source_ref_parts.commit_id
892 source_ref_id = pull_request.source_ref_parts.commit_id
893 source_repo = pull_request.source_repo.scm_instance()
893 source_repo = pull_request.source_repo.scm_instance()
894
894
895 try:
895 try:
896 if source_ref_type in self.REF_TYPES:
896 if source_ref_type in self.REF_TYPES:
897 source_commit = source_repo.get_commit(source_ref_name)
897 source_commit = source_repo.get_commit(source_ref_name)
898 else:
898 else:
899 source_commit = source_repo.get_commit(source_ref_id)
899 source_commit = source_repo.get_commit(source_ref_id)
900 except CommitDoesNotExistError:
900 except CommitDoesNotExistError:
901 raise SourceRefMissing()
901 raise SourceRefMissing()
902
902
903 # target repo
903 # target repo
904 target_ref_name = pull_request.target_ref_parts.name
904 target_ref_name = pull_request.target_ref_parts.name
905 target_ref_type = pull_request.target_ref_parts.type
905 target_ref_type = pull_request.target_ref_parts.type
906 target_ref_id = pull_request.target_ref_parts.commit_id
906 target_ref_id = pull_request.target_ref_parts.commit_id
907 target_repo = pull_request.target_repo.scm_instance()
907 target_repo = pull_request.target_repo.scm_instance()
908
908
909 try:
909 try:
910 if target_ref_type in self.REF_TYPES:
910 if target_ref_type in self.REF_TYPES:
911 target_commit = target_repo.get_commit(target_ref_name)
911 target_commit = target_repo.get_commit(target_ref_name)
912 else:
912 else:
913 target_commit = target_repo.get_commit(target_ref_id)
913 target_commit = target_repo.get_commit(target_ref_id)
914 except CommitDoesNotExistError:
914 except CommitDoesNotExistError:
915 raise TargetRefMissing()
915 raise TargetRefMissing()
916
916
917 return source_commit, target_commit
917 return source_commit, target_commit
918
918
919 def update_commits(self, pull_request, updating_user):
919 def update_commits(self, pull_request, updating_user):
920 """
920 """
921 Get the updated list of commits for the pull request
921 Get the updated list of commits for the pull request
922 and return the new pull request version and the list
922 and return the new pull request version and the list
923 of commits processed by this update action
923 of commits processed by this update action
924
924
925 updating_user is the user_object who triggered the update
925 updating_user is the user_object who triggered the update
926 """
926 """
927 pull_request = self.__get_pull_request(pull_request)
927 pull_request = self.__get_pull_request(pull_request)
928 source_ref_type = pull_request.source_ref_parts.type
928 source_ref_type = pull_request.source_ref_parts.type
929 source_ref_name = pull_request.source_ref_parts.name
929 source_ref_name = pull_request.source_ref_parts.name
930 source_ref_id = pull_request.source_ref_parts.commit_id
930 source_ref_id = pull_request.source_ref_parts.commit_id
931
931
932 target_ref_type = pull_request.target_ref_parts.type
932 target_ref_type = pull_request.target_ref_parts.type
933 target_ref_name = pull_request.target_ref_parts.name
933 target_ref_name = pull_request.target_ref_parts.name
934 target_ref_id = pull_request.target_ref_parts.commit_id
934 target_ref_id = pull_request.target_ref_parts.commit_id
935
935
936 if not self.has_valid_update_type(pull_request):
936 if not self.has_valid_update_type(pull_request):
937 log.debug("Skipping update of pull request %s due to ref type: %s",
937 log.debug("Skipping update of pull request %s due to ref type: %s",
938 pull_request, source_ref_type)
938 pull_request, source_ref_type)
939 return UpdateResponse(
939 return UpdateResponse(
940 executed=False,
940 executed=False,
941 reason=UpdateFailureReason.WRONG_REF_TYPE,
941 reason=UpdateFailureReason.WRONG_REF_TYPE,
942 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
942 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
943 source_changed=False, target_changed=False)
943 source_changed=False, target_changed=False)
944
944
945 try:
945 try:
946 source_commit, target_commit = self.get_flow_commits(pull_request)
946 source_commit, target_commit = self.get_flow_commits(pull_request)
947 except SourceRefMissing:
947 except SourceRefMissing:
948 return UpdateResponse(
948 return UpdateResponse(
949 executed=False,
949 executed=False,
950 reason=UpdateFailureReason.MISSING_SOURCE_REF,
950 reason=UpdateFailureReason.MISSING_SOURCE_REF,
951 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
951 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
952 source_changed=False, target_changed=False)
952 source_changed=False, target_changed=False)
953 except TargetRefMissing:
953 except TargetRefMissing:
954 return UpdateResponse(
954 return UpdateResponse(
955 executed=False,
955 executed=False,
956 reason=UpdateFailureReason.MISSING_TARGET_REF,
956 reason=UpdateFailureReason.MISSING_TARGET_REF,
957 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
957 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
958 source_changed=False, target_changed=False)
958 source_changed=False, target_changed=False)
959
959
960 source_changed = source_ref_id != source_commit.raw_id
960 source_changed = source_ref_id != source_commit.raw_id
961 target_changed = target_ref_id != target_commit.raw_id
961 target_changed = target_ref_id != target_commit.raw_id
962
962
963 if not (source_changed or target_changed):
963 if not (source_changed or target_changed):
964 log.debug("Nothing changed in pull request %s", pull_request)
964 log.debug("Nothing changed in pull request %s", pull_request)
965 return UpdateResponse(
965 return UpdateResponse(
966 executed=False,
966 executed=False,
967 reason=UpdateFailureReason.NO_CHANGE,
967 reason=UpdateFailureReason.NO_CHANGE,
968 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
968 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
969 source_changed=target_changed, target_changed=source_changed)
969 source_changed=target_changed, target_changed=source_changed)
970
970
971 change_in_found = 'target repo' if target_changed else 'source repo'
971 change_in_found = 'target repo' if target_changed else 'source repo'
972 log.debug('Updating pull request because of change in %s detected',
972 log.debug('Updating pull request because of change in %s detected',
973 change_in_found)
973 change_in_found)
974
974
975 # Finally there is a need for an update, in case of source change
975 # Finally there is a need for an update, in case of source change
976 # we create a new version, else just an update
976 # we create a new version, else just an update
977 if source_changed:
977 if source_changed:
978 pull_request_version = self._create_version_from_snapshot(pull_request)
978 pull_request_version = self._create_version_from_snapshot(pull_request)
979 self._link_comments_to_version(pull_request_version)
979 self._link_comments_to_version(pull_request_version)
980 else:
980 else:
981 try:
981 try:
982 ver = pull_request.versions[-1]
982 ver = pull_request.versions[-1]
983 except IndexError:
983 except IndexError:
984 ver = None
984 ver = None
985
985
986 pull_request.pull_request_version_id = \
986 pull_request.pull_request_version_id = \
987 ver.pull_request_version_id if ver else None
987 ver.pull_request_version_id if ver else None
988 pull_request_version = pull_request
988 pull_request_version = pull_request
989
989
990 source_repo = pull_request.source_repo.scm_instance()
990 source_repo = pull_request.source_repo.scm_instance()
991 target_repo = pull_request.target_repo.scm_instance()
991 target_repo = pull_request.target_repo.scm_instance()
992
992
993 # re-compute commit ids
993 # re-compute commit ids
994 old_commit_ids = pull_request.revisions
994 old_commit_ids = pull_request.revisions
995 pre_load = ["author", "date", "message", "branch"]
995 pre_load = ["author", "date", "message", "branch"]
996 commit_ranges = target_repo.compare(
996 commit_ranges = target_repo.compare(
997 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
997 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
998 pre_load=pre_load)
998 pre_load=pre_load)
999
999
1000 target_ref = target_commit.raw_id
1000 target_ref = target_commit.raw_id
1001 source_ref = source_commit.raw_id
1001 source_ref = source_commit.raw_id
1002 ancestor_commit_id = target_repo.get_common_ancestor(
1002 ancestor_commit_id = target_repo.get_common_ancestor(
1003 target_ref, source_ref, source_repo)
1003 target_ref, source_ref, source_repo)
1004
1004
1005 if not ancestor_commit_id:
1005 if not ancestor_commit_id:
1006 raise ValueError(
1006 raise ValueError(
1007 'cannot calculate diff info without a common ancestor. '
1007 'cannot calculate diff info without a common ancestor. '
1008 'Make sure both repositories are related, and have a common forking commit.')
1008 'Make sure both repositories are related, and have a common forking commit.')
1009
1009
1010 pull_request.common_ancestor_id = ancestor_commit_id
1010 pull_request.common_ancestor_id = ancestor_commit_id
1011
1011
1012 pull_request.source_ref = '%s:%s:%s' % (
1012 pull_request.source_ref = '%s:%s:%s' % (
1013 source_ref_type, source_ref_name, source_commit.raw_id)
1013 source_ref_type, source_ref_name, source_commit.raw_id)
1014 pull_request.target_ref = '%s:%s:%s' % (
1014 pull_request.target_ref = '%s:%s:%s' % (
1015 target_ref_type, target_ref_name, ancestor_commit_id)
1015 target_ref_type, target_ref_name, ancestor_commit_id)
1016
1016
1017 pull_request.revisions = [
1017 pull_request.revisions = [
1018 commit.raw_id for commit in reversed(commit_ranges)]
1018 commit.raw_id for commit in reversed(commit_ranges)]
1019 pull_request.updated_on = datetime.datetime.now()
1019 pull_request.updated_on = datetime.datetime.now()
1020 Session().add(pull_request)
1020 Session().add(pull_request)
1021 new_commit_ids = pull_request.revisions
1021 new_commit_ids = pull_request.revisions
1022
1022
1023 old_diff_data, new_diff_data = self._generate_update_diffs(
1023 old_diff_data, new_diff_data = self._generate_update_diffs(
1024 pull_request, pull_request_version)
1024 pull_request, pull_request_version)
1025
1025
1026 # calculate commit and file changes
1026 # calculate commit and file changes
1027 commit_changes = self._calculate_commit_id_changes(
1027 commit_changes = self._calculate_commit_id_changes(
1028 old_commit_ids, new_commit_ids)
1028 old_commit_ids, new_commit_ids)
1029 file_changes = self._calculate_file_changes(
1029 file_changes = self._calculate_file_changes(
1030 old_diff_data, new_diff_data)
1030 old_diff_data, new_diff_data)
1031
1031
1032 # set comments as outdated if DIFFS changed
1032 # set comments as outdated if DIFFS changed
1033 CommentsModel().outdate_comments(
1033 CommentsModel().outdate_comments(
1034 pull_request, old_diff_data=old_diff_data,
1034 pull_request, old_diff_data=old_diff_data,
1035 new_diff_data=new_diff_data)
1035 new_diff_data=new_diff_data)
1036
1036
1037 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1037 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1038 file_node_changes = (
1038 file_node_changes = (
1039 file_changes.added or file_changes.modified or file_changes.removed)
1039 file_changes.added or file_changes.modified or file_changes.removed)
1040 pr_has_changes = valid_commit_changes or file_node_changes
1040 pr_has_changes = valid_commit_changes or file_node_changes
1041
1041
1042 # Add an automatic comment to the pull request, in case
1042 # Add an automatic comment to the pull request, in case
1043 # anything has changed
1043 # anything has changed
1044 if pr_has_changes:
1044 if pr_has_changes:
1045 update_comment = CommentsModel().create(
1045 update_comment = CommentsModel().create(
1046 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1046 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1047 repo=pull_request.target_repo,
1047 repo=pull_request.target_repo,
1048 user=pull_request.author,
1048 user=pull_request.author,
1049 pull_request=pull_request,
1049 pull_request=pull_request,
1050 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1050 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1051
1051
1052 # Update status to "Under Review" for added commits
1052 # Update status to "Under Review" for added commits
1053 for commit_id in commit_changes.added:
1053 for commit_id in commit_changes.added:
1054 ChangesetStatusModel().set_status(
1054 ChangesetStatusModel().set_status(
1055 repo=pull_request.source_repo,
1055 repo=pull_request.source_repo,
1056 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1056 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1057 comment=update_comment,
1057 comment=update_comment,
1058 user=pull_request.author,
1058 user=pull_request.author,
1059 pull_request=pull_request,
1059 pull_request=pull_request,
1060 revision=commit_id)
1060 revision=commit_id)
1061
1061
1062 # send update email to users
1062 # send update email to users
1063 try:
1063 try:
1064 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1064 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1065 ancestor_commit_id=ancestor_commit_id,
1065 ancestor_commit_id=ancestor_commit_id,
1066 commit_changes=commit_changes,
1066 commit_changes=commit_changes,
1067 file_changes=file_changes)
1067 file_changes=file_changes)
1068 except Exception:
1068 except Exception:
1069 log.exception('Failed to send email notification to users')
1069 log.exception('Failed to send email notification to users')
1070
1070
1071 log.debug(
1071 log.debug(
1072 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1072 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1073 'removed_ids: %s', pull_request.pull_request_id,
1073 'removed_ids: %s', pull_request.pull_request_id,
1074 commit_changes.added, commit_changes.common, commit_changes.removed)
1074 commit_changes.added, commit_changes.common, commit_changes.removed)
1075 log.debug(
1075 log.debug(
1076 'Updated pull request with the following file changes: %s',
1076 'Updated pull request with the following file changes: %s',
1077 file_changes)
1077 file_changes)
1078
1078
1079 log.info(
1079 log.info(
1080 "Updated pull request %s from commit %s to commit %s, "
1080 "Updated pull request %s from commit %s to commit %s, "
1081 "stored new version %s of this pull request.",
1081 "stored new version %s of this pull request.",
1082 pull_request.pull_request_id, source_ref_id,
1082 pull_request.pull_request_id, source_ref_id,
1083 pull_request.source_ref_parts.commit_id,
1083 pull_request.source_ref_parts.commit_id,
1084 pull_request_version.pull_request_version_id)
1084 pull_request_version.pull_request_version_id)
1085 Session().commit()
1085 Session().commit()
1086 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1086 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1087
1087
1088 return UpdateResponse(
1088 return UpdateResponse(
1089 executed=True, reason=UpdateFailureReason.NONE,
1089 executed=True, reason=UpdateFailureReason.NONE,
1090 old=pull_request, new=pull_request_version,
1090 old=pull_request, new=pull_request_version,
1091 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1091 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1092 source_changed=source_changed, target_changed=target_changed)
1092 source_changed=source_changed, target_changed=target_changed)
1093
1093
1094 def _create_version_from_snapshot(self, pull_request):
1094 def _create_version_from_snapshot(self, pull_request):
1095 version = PullRequestVersion()
1095 version = PullRequestVersion()
1096 version.title = pull_request.title
1096 version.title = pull_request.title
1097 version.description = pull_request.description
1097 version.description = pull_request.description
1098 version.status = pull_request.status
1098 version.status = pull_request.status
1099 version.pull_request_state = pull_request.pull_request_state
1099 version.pull_request_state = pull_request.pull_request_state
1100 version.created_on = datetime.datetime.now()
1100 version.created_on = datetime.datetime.now()
1101 version.updated_on = pull_request.updated_on
1101 version.updated_on = pull_request.updated_on
1102 version.user_id = pull_request.user_id
1102 version.user_id = pull_request.user_id
1103 version.source_repo = pull_request.source_repo
1103 version.source_repo = pull_request.source_repo
1104 version.source_ref = pull_request.source_ref
1104 version.source_ref = pull_request.source_ref
1105 version.target_repo = pull_request.target_repo
1105 version.target_repo = pull_request.target_repo
1106 version.target_ref = pull_request.target_ref
1106 version.target_ref = pull_request.target_ref
1107
1107
1108 version._last_merge_source_rev = pull_request._last_merge_source_rev
1108 version._last_merge_source_rev = pull_request._last_merge_source_rev
1109 version._last_merge_target_rev = pull_request._last_merge_target_rev
1109 version._last_merge_target_rev = pull_request._last_merge_target_rev
1110 version.last_merge_status = pull_request.last_merge_status
1110 version.last_merge_status = pull_request.last_merge_status
1111 version.last_merge_metadata = pull_request.last_merge_metadata
1111 version.last_merge_metadata = pull_request.last_merge_metadata
1112 version.shadow_merge_ref = pull_request.shadow_merge_ref
1112 version.shadow_merge_ref = pull_request.shadow_merge_ref
1113 version.merge_rev = pull_request.merge_rev
1113 version.merge_rev = pull_request.merge_rev
1114 version.reviewer_data = pull_request.reviewer_data
1114 version.reviewer_data = pull_request.reviewer_data
1115
1115
1116 version.revisions = pull_request.revisions
1116 version.revisions = pull_request.revisions
1117 version.common_ancestor_id = pull_request.common_ancestor_id
1117 version.common_ancestor_id = pull_request.common_ancestor_id
1118 version.pull_request = pull_request
1118 version.pull_request = pull_request
1119 Session().add(version)
1119 Session().add(version)
1120 Session().flush()
1120 Session().flush()
1121
1121
1122 return version
1122 return version
1123
1123
1124 def _generate_update_diffs(self, pull_request, pull_request_version):
1124 def _generate_update_diffs(self, pull_request, pull_request_version):
1125
1125
1126 diff_context = (
1126 diff_context = (
1127 self.DIFF_CONTEXT +
1127 self.DIFF_CONTEXT +
1128 CommentsModel.needed_extra_diff_context())
1128 CommentsModel.needed_extra_diff_context())
1129 hide_whitespace_changes = False
1129 hide_whitespace_changes = False
1130 source_repo = pull_request_version.source_repo
1130 source_repo = pull_request_version.source_repo
1131 source_ref_id = pull_request_version.source_ref_parts.commit_id
1131 source_ref_id = pull_request_version.source_ref_parts.commit_id
1132 target_ref_id = pull_request_version.target_ref_parts.commit_id
1132 target_ref_id = pull_request_version.target_ref_parts.commit_id
1133 old_diff = self._get_diff_from_pr_or_version(
1133 old_diff = self._get_diff_from_pr_or_version(
1134 source_repo, source_ref_id, target_ref_id,
1134 source_repo, source_ref_id, target_ref_id,
1135 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1135 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1136
1136
1137 source_repo = pull_request.source_repo
1137 source_repo = pull_request.source_repo
1138 source_ref_id = pull_request.source_ref_parts.commit_id
1138 source_ref_id = pull_request.source_ref_parts.commit_id
1139 target_ref_id = pull_request.target_ref_parts.commit_id
1139 target_ref_id = pull_request.target_ref_parts.commit_id
1140
1140
1141 new_diff = self._get_diff_from_pr_or_version(
1141 new_diff = self._get_diff_from_pr_or_version(
1142 source_repo, source_ref_id, target_ref_id,
1142 source_repo, source_ref_id, target_ref_id,
1143 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1143 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1144
1144
1145 old_diff_data = diffs.DiffProcessor(old_diff)
1145 old_diff_data = diffs.DiffProcessor(old_diff)
1146 old_diff_data.prepare()
1146 old_diff_data.prepare()
1147 new_diff_data = diffs.DiffProcessor(new_diff)
1147 new_diff_data = diffs.DiffProcessor(new_diff)
1148 new_diff_data.prepare()
1148 new_diff_data.prepare()
1149
1149
1150 return old_diff_data, new_diff_data
1150 return old_diff_data, new_diff_data
1151
1151
1152 def _link_comments_to_version(self, pull_request_version):
1152 def _link_comments_to_version(self, pull_request_version):
1153 """
1153 """
1154 Link all unlinked comments of this pull request to the given version.
1154 Link all unlinked comments of this pull request to the given version.
1155
1155
1156 :param pull_request_version: The `PullRequestVersion` to which
1156 :param pull_request_version: The `PullRequestVersion` to which
1157 the comments shall be linked.
1157 the comments shall be linked.
1158
1158
1159 """
1159 """
1160 pull_request = pull_request_version.pull_request
1160 pull_request = pull_request_version.pull_request
1161 comments = ChangesetComment.query()\
1161 comments = ChangesetComment.query()\
1162 .filter(
1162 .filter(
1163 # TODO: johbo: Should we query for the repo at all here?
1163 # TODO: johbo: Should we query for the repo at all here?
1164 # Pending decision on how comments of PRs are to be related
1164 # Pending decision on how comments of PRs are to be related
1165 # to either the source repo, the target repo or no repo at all.
1165 # to either the source repo, the target repo or no repo at all.
1166 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1166 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1167 ChangesetComment.pull_request == pull_request,
1167 ChangesetComment.pull_request == pull_request,
1168 ChangesetComment.pull_request_version == None)\
1168 ChangesetComment.pull_request_version == None)\
1169 .order_by(ChangesetComment.comment_id.asc())
1169 .order_by(ChangesetComment.comment_id.asc())
1170
1170
1171 # TODO: johbo: Find out why this breaks if it is done in a bulk
1171 # TODO: johbo: Find out why this breaks if it is done in a bulk
1172 # operation.
1172 # operation.
1173 for comment in comments:
1173 for comment in comments:
1174 comment.pull_request_version_id = (
1174 comment.pull_request_version_id = (
1175 pull_request_version.pull_request_version_id)
1175 pull_request_version.pull_request_version_id)
1176 Session().add(comment)
1176 Session().add(comment)
1177
1177
1178 def _calculate_commit_id_changes(self, old_ids, new_ids):
1178 def _calculate_commit_id_changes(self, old_ids, new_ids):
1179 added = [x for x in new_ids if x not in old_ids]
1179 added = [x for x in new_ids if x not in old_ids]
1180 common = [x for x in new_ids if x in old_ids]
1180 common = [x for x in new_ids if x in old_ids]
1181 removed = [x for x in old_ids if x not in new_ids]
1181 removed = [x for x in old_ids if x not in new_ids]
1182 total = new_ids
1182 total = new_ids
1183 return ChangeTuple(added, common, removed, total)
1183 return ChangeTuple(added, common, removed, total)
1184
1184
1185 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1185 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1186
1186
1187 old_files = OrderedDict()
1187 old_files = OrderedDict()
1188 for diff_data in old_diff_data.parsed_diff:
1188 for diff_data in old_diff_data.parsed_diff:
1189 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1189 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1190
1190
1191 added_files = []
1191 added_files = []
1192 modified_files = []
1192 modified_files = []
1193 removed_files = []
1193 removed_files = []
1194 for diff_data in new_diff_data.parsed_diff:
1194 for diff_data in new_diff_data.parsed_diff:
1195 new_filename = diff_data['filename']
1195 new_filename = diff_data['filename']
1196 new_hash = md5_safe(diff_data['raw_diff'])
1196 new_hash = md5_safe(diff_data['raw_diff'])
1197
1197
1198 old_hash = old_files.get(new_filename)
1198 old_hash = old_files.get(new_filename)
1199 if not old_hash:
1199 if not old_hash:
1200 # file is not present in old diff, we have to figure out from parsed diff
1200 # file is not present in old diff, we have to figure out from parsed diff
1201 # operation ADD/REMOVE
1201 # operation ADD/REMOVE
1202 operations_dict = diff_data['stats']['ops']
1202 operations_dict = diff_data['stats']['ops']
1203 if diffs.DEL_FILENODE in operations_dict:
1203 if diffs.DEL_FILENODE in operations_dict:
1204 removed_files.append(new_filename)
1204 removed_files.append(new_filename)
1205 else:
1205 else:
1206 added_files.append(new_filename)
1206 added_files.append(new_filename)
1207 else:
1207 else:
1208 if new_hash != old_hash:
1208 if new_hash != old_hash:
1209 modified_files.append(new_filename)
1209 modified_files.append(new_filename)
1210 # now remove a file from old, since we have seen it already
1210 # now remove a file from old, since we have seen it already
1211 del old_files[new_filename]
1211 del old_files[new_filename]
1212
1212
1213 # removed files is when there are present in old, but not in NEW,
1213 # removed files is when there are present in old, but not in NEW,
1214 # since we remove old files that are present in new diff, left-overs
1214 # since we remove old files that are present in new diff, left-overs
1215 # if any should be the removed files
1215 # if any should be the removed files
1216 removed_files.extend(old_files.keys())
1216 removed_files.extend(old_files.keys())
1217
1217
1218 return FileChangeTuple(added_files, modified_files, removed_files)
1218 return FileChangeTuple(added_files, modified_files, removed_files)
1219
1219
1220 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1220 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1221 """
1221 """
1222 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1222 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1223 so it's always looking the same disregarding on which default
1223 so it's always looking the same disregarding on which default
1224 renderer system is using.
1224 renderer system is using.
1225
1225
1226 :param ancestor_commit_id: ancestor raw_id
1226 :param ancestor_commit_id: ancestor raw_id
1227 :param changes: changes named tuple
1227 :param changes: changes named tuple
1228 :param file_changes: file changes named tuple
1228 :param file_changes: file changes named tuple
1229
1229
1230 """
1230 """
1231 new_status = ChangesetStatus.get_status_lbl(
1231 new_status = ChangesetStatus.get_status_lbl(
1232 ChangesetStatus.STATUS_UNDER_REVIEW)
1232 ChangesetStatus.STATUS_UNDER_REVIEW)
1233
1233
1234 changed_files = (
1234 changed_files = (
1235 file_changes.added + file_changes.modified + file_changes.removed)
1235 file_changes.added + file_changes.modified + file_changes.removed)
1236
1236
1237 params = {
1237 params = {
1238 'under_review_label': new_status,
1238 'under_review_label': new_status,
1239 'added_commits': changes.added,
1239 'added_commits': changes.added,
1240 'removed_commits': changes.removed,
1240 'removed_commits': changes.removed,
1241 'changed_files': changed_files,
1241 'changed_files': changed_files,
1242 'added_files': file_changes.added,
1242 'added_files': file_changes.added,
1243 'modified_files': file_changes.modified,
1243 'modified_files': file_changes.modified,
1244 'removed_files': file_changes.removed,
1244 'removed_files': file_changes.removed,
1245 'ancestor_commit_id': ancestor_commit_id
1245 'ancestor_commit_id': ancestor_commit_id
1246 }
1246 }
1247 renderer = RstTemplateRenderer()
1247 renderer = RstTemplateRenderer()
1248 return renderer.render('pull_request_update.mako', **params)
1248 return renderer.render('pull_request_update.mako', **params)
1249
1249
1250 def edit(self, pull_request, title, description, description_renderer, user):
1250 def edit(self, pull_request, title, description, description_renderer, user):
1251 pull_request = self.__get_pull_request(pull_request)
1251 pull_request = self.__get_pull_request(pull_request)
1252 old_data = pull_request.get_api_data(with_merge_state=False)
1252 old_data = pull_request.get_api_data(with_merge_state=False)
1253 if pull_request.is_closed():
1253 if pull_request.is_closed():
1254 raise ValueError('This pull request is closed')
1254 raise ValueError('This pull request is closed')
1255 if title:
1255 if title:
1256 pull_request.title = title
1256 pull_request.title = title
1257 pull_request.description = description
1257 pull_request.description = description
1258 pull_request.updated_on = datetime.datetime.now()
1258 pull_request.updated_on = datetime.datetime.now()
1259 pull_request.description_renderer = description_renderer
1259 pull_request.description_renderer = description_renderer
1260 Session().add(pull_request)
1260 Session().add(pull_request)
1261 self._log_audit_action(
1261 self._log_audit_action(
1262 'repo.pull_request.edit', {'old_data': old_data},
1262 'repo.pull_request.edit', {'old_data': old_data},
1263 user, pull_request)
1263 user, pull_request)
1264
1264
1265 def update_reviewers(self, pull_request, reviewer_data, user):
1265 def update_reviewers(self, pull_request, reviewer_data, user):
1266 """
1266 """
1267 Update the reviewers in the pull request
1267 Update the reviewers in the pull request
1268
1268
1269 :param pull_request: the pr to update
1269 :param pull_request: the pr to update
1270 :param reviewer_data: list of tuples
1270 :param reviewer_data: list of tuples
1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1272 :param user: current use who triggers this action
1272 :param user: current use who triggers this action
1273 """
1273 """
1274
1274
1275 pull_request = self.__get_pull_request(pull_request)
1275 pull_request = self.__get_pull_request(pull_request)
1276 if pull_request.is_closed():
1276 if pull_request.is_closed():
1277 raise ValueError('This pull request is closed')
1277 raise ValueError('This pull request is closed')
1278
1278
1279 reviewers = {}
1279 reviewers = {}
1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1281 if isinstance(user_id, (int, compat.string_types)):
1281 if isinstance(user_id, (int, compat.string_types)):
1282 user_id = self._get_user(user_id).user_id
1282 user_id = self._get_user(user_id).user_id
1283 reviewers[user_id] = {
1283 reviewers[user_id] = {
1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1285
1285
1286 reviewers_ids = set(reviewers.keys())
1286 reviewers_ids = set(reviewers.keys())
1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1289
1289
1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1291
1291
1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1293 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1293 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1294
1294
1295 log.debug("Adding %s reviewers", ids_to_add)
1295 log.debug("Adding %s reviewers", ids_to_add)
1296 log.debug("Removing %s reviewers", ids_to_remove)
1296 log.debug("Removing %s reviewers", ids_to_remove)
1297 changed = False
1297 changed = False
1298 added_audit_reviewers = []
1298 added_audit_reviewers = []
1299 removed_audit_reviewers = []
1299 removed_audit_reviewers = []
1300
1300
1301 for uid in ids_to_add:
1301 for uid in ids_to_add:
1302 changed = True
1302 changed = True
1303 _usr = self._get_user(uid)
1303 _usr = self._get_user(uid)
1304 reviewer = PullRequestReviewers()
1304 reviewer = PullRequestReviewers()
1305 reviewer.user = _usr
1305 reviewer.user = _usr
1306 reviewer.pull_request = pull_request
1306 reviewer.pull_request = pull_request
1307 reviewer.reasons = reviewers[uid]['reasons']
1307 reviewer.reasons = reviewers[uid]['reasons']
1308 # NOTE(marcink): mandatory shouldn't be changed now
1308 # NOTE(marcink): mandatory shouldn't be changed now
1309 # reviewer.mandatory = reviewers[uid]['reasons']
1309 # reviewer.mandatory = reviewers[uid]['reasons']
1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1312 Session().add(reviewer)
1312 Session().add(reviewer)
1313 added_audit_reviewers.append(reviewer.get_dict())
1313 added_audit_reviewers.append(reviewer.get_dict())
1314
1314
1315 for uid in ids_to_remove:
1315 for uid in ids_to_remove:
1316 changed = True
1316 changed = True
1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1318 # This is an edge case that handles previous state of having the same reviewer twice.
1318 # This is an edge case that handles previous state of having the same reviewer twice.
1319 # this CAN happen due to the lack of DB checks
1319 # this CAN happen due to the lack of DB checks
1320 reviewers = PullRequestReviewers.query()\
1320 reviewers = PullRequestReviewers.query()\
1321 .filter(PullRequestReviewers.user_id == uid,
1321 .filter(PullRequestReviewers.user_id == uid,
1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1323 PullRequestReviewers.pull_request == pull_request)\
1323 PullRequestReviewers.pull_request == pull_request)\
1324 .all()
1324 .all()
1325
1325
1326 for obj in reviewers:
1326 for obj in reviewers:
1327 added_audit_reviewers.append(obj.get_dict())
1327 added_audit_reviewers.append(obj.get_dict())
1328 Session().delete(obj)
1328 Session().delete(obj)
1329
1329
1330 if changed:
1330 if changed:
1331 Session().expire_all()
1331 Session().expire_all()
1332 pull_request.updated_on = datetime.datetime.now()
1332 pull_request.updated_on = datetime.datetime.now()
1333 Session().add(pull_request)
1333 Session().add(pull_request)
1334
1334
1335 # finally store audit logs
1335 # finally store audit logs
1336 for user_data in added_audit_reviewers:
1336 for user_data in added_audit_reviewers:
1337 self._log_audit_action(
1337 self._log_audit_action(
1338 'repo.pull_request.reviewer.add', {'data': user_data},
1338 'repo.pull_request.reviewer.add', {'data': user_data},
1339 user, pull_request)
1339 user, pull_request)
1340 for user_data in removed_audit_reviewers:
1340 for user_data in removed_audit_reviewers:
1341 self._log_audit_action(
1341 self._log_audit_action(
1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1343 user, pull_request)
1343 user, pull_request)
1344
1344
1345 self.notify_reviewers(pull_request, ids_to_add, user)
1345 self.notify_reviewers(pull_request, ids_to_add, user)
1346 return ids_to_add, ids_to_remove
1346 return ids_to_add, ids_to_remove
1347
1347
1348 def update_observers(self, pull_request, observer_data, user):
1348 def update_observers(self, pull_request, observer_data, user):
1349 """
1349 """
1350 Update the observers in the pull request
1350 Update the observers in the pull request
1351
1351
1352 :param pull_request: the pr to update
1352 :param pull_request: the pr to update
1353 :param observer_data: list of tuples
1353 :param observer_data: list of tuples
1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1355 :param user: current use who triggers this action
1355 :param user: current use who triggers this action
1356 """
1356 """
1357 pull_request = self.__get_pull_request(pull_request)
1357 pull_request = self.__get_pull_request(pull_request)
1358 if pull_request.is_closed():
1358 if pull_request.is_closed():
1359 raise ValueError('This pull request is closed')
1359 raise ValueError('This pull request is closed')
1360
1360
1361 observers = {}
1361 observers = {}
1362 for user_id, reasons, mandatory, role, rules in observer_data:
1362 for user_id, reasons, mandatory, role, rules in observer_data:
1363 if isinstance(user_id, (int, compat.string_types)):
1363 if isinstance(user_id, (int, compat.string_types)):
1364 user_id = self._get_user(user_id).user_id
1364 user_id = self._get_user(user_id).user_id
1365 observers[user_id] = {
1365 observers[user_id] = {
1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1367
1367
1368 observers_ids = set(observers.keys())
1368 observers_ids = set(observers.keys())
1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1371
1371
1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1373
1373
1374 ids_to_add = observers_ids.difference(current_observers_ids)
1374 ids_to_add = observers_ids.difference(current_observers_ids)
1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1376
1376
1377 log.debug("Adding %s observer", ids_to_add)
1377 log.debug("Adding %s observer", ids_to_add)
1378 log.debug("Removing %s observer", ids_to_remove)
1378 log.debug("Removing %s observer", ids_to_remove)
1379 changed = False
1379 changed = False
1380 added_audit_observers = []
1380 added_audit_observers = []
1381 removed_audit_observers = []
1381 removed_audit_observers = []
1382
1382
1383 for uid in ids_to_add:
1383 for uid in ids_to_add:
1384 changed = True
1384 changed = True
1385 _usr = self._get_user(uid)
1385 _usr = self._get_user(uid)
1386 observer = PullRequestReviewers()
1386 observer = PullRequestReviewers()
1387 observer.user = _usr
1387 observer.user = _usr
1388 observer.pull_request = pull_request
1388 observer.pull_request = pull_request
1389 observer.reasons = observers[uid]['reasons']
1389 observer.reasons = observers[uid]['reasons']
1390 # NOTE(marcink): mandatory shouldn't be changed now
1390 # NOTE(marcink): mandatory shouldn't be changed now
1391 # observer.mandatory = observer[uid]['reasons']
1391 # observer.mandatory = observer[uid]['reasons']
1392
1392
1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1395 Session().add(observer)
1395 Session().add(observer)
1396 added_audit_observers.append(observer.get_dict())
1396 added_audit_observers.append(observer.get_dict())
1397
1397
1398 for uid in ids_to_remove:
1398 for uid in ids_to_remove:
1399 changed = True
1399 changed = True
1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1401 # This is an edge case that handles previous state of having the same reviewer twice.
1401 # This is an edge case that handles previous state of having the same reviewer twice.
1402 # this CAN happen due to the lack of DB checks
1402 # this CAN happen due to the lack of DB checks
1403 observers = PullRequestReviewers.query()\
1403 observers = PullRequestReviewers.query()\
1404 .filter(PullRequestReviewers.user_id == uid,
1404 .filter(PullRequestReviewers.user_id == uid,
1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1406 PullRequestReviewers.pull_request == pull_request)\
1406 PullRequestReviewers.pull_request == pull_request)\
1407 .all()
1407 .all()
1408
1408
1409 for obj in observers:
1409 for obj in observers:
1410 added_audit_observers.append(obj.get_dict())
1410 added_audit_observers.append(obj.get_dict())
1411 Session().delete(obj)
1411 Session().delete(obj)
1412
1412
1413 if changed:
1413 if changed:
1414 Session().expire_all()
1414 Session().expire_all()
1415 pull_request.updated_on = datetime.datetime.now()
1415 pull_request.updated_on = datetime.datetime.now()
1416 Session().add(pull_request)
1416 Session().add(pull_request)
1417
1417
1418 # finally store audit logs
1418 # finally store audit logs
1419 for user_data in added_audit_observers:
1419 for user_data in added_audit_observers:
1420 self._log_audit_action(
1420 self._log_audit_action(
1421 'repo.pull_request.observer.add', {'data': user_data},
1421 'repo.pull_request.observer.add', {'data': user_data},
1422 user, pull_request)
1422 user, pull_request)
1423 for user_data in removed_audit_observers:
1423 for user_data in removed_audit_observers:
1424 self._log_audit_action(
1424 self._log_audit_action(
1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1426 user, pull_request)
1426 user, pull_request)
1427
1427
1428 self.notify_observers(pull_request, ids_to_add, user)
1428 self.notify_observers(pull_request, ids_to_add, user)
1429 return ids_to_add, ids_to_remove
1429 return ids_to_add, ids_to_remove
1430
1430
1431 def get_url(self, pull_request, request=None, permalink=False):
1431 def get_url(self, pull_request, request=None, permalink=False):
1432 if not request:
1432 if not request:
1433 request = get_current_request()
1433 request = get_current_request()
1434
1434
1435 if permalink:
1435 if permalink:
1436 return request.route_url(
1436 return request.route_url(
1437 'pull_requests_global',
1437 'pull_requests_global',
1438 pull_request_id=pull_request.pull_request_id,)
1438 pull_request_id=pull_request.pull_request_id,)
1439 else:
1439 else:
1440 return request.route_url('pullrequest_show',
1440 return request.route_url('pullrequest_show',
1441 repo_name=safe_str(pull_request.target_repo.repo_name),
1441 repo_name=safe_str(pull_request.target_repo.repo_name),
1442 pull_request_id=pull_request.pull_request_id,)
1442 pull_request_id=pull_request.pull_request_id,)
1443
1443
1444 def get_shadow_clone_url(self, pull_request, request=None):
1444 def get_shadow_clone_url(self, pull_request, request=None):
1445 """
1445 """
1446 Returns qualified url pointing to the shadow repository. If this pull
1446 Returns qualified url pointing to the shadow repository. If this pull
1447 request is closed there is no shadow repository and ``None`` will be
1447 request is closed there is no shadow repository and ``None`` will be
1448 returned.
1448 returned.
1449 """
1449 """
1450 if pull_request.is_closed():
1450 if pull_request.is_closed():
1451 return None
1451 return None
1452 else:
1452 else:
1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1455
1455
1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1457 # notification to reviewers/observers
1457 # notification to reviewers/observers
1458 if not user_ids:
1458 if not user_ids:
1459 return
1459 return
1460
1460
1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1462
1462
1463 pull_request_obj = pull_request
1463 pull_request_obj = pull_request
1464 # get the current participants of this pull request
1464 # get the current participants of this pull request
1465 recipients = user_ids
1465 recipients = user_ids
1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1467
1467
1468 pr_source_repo = pull_request_obj.source_repo
1468 pr_source_repo = pull_request_obj.source_repo
1469 pr_target_repo = pull_request_obj.target_repo
1469 pr_target_repo = pull_request_obj.target_repo
1470
1470
1471 pr_url = h.route_url('pullrequest_show',
1471 pr_url = h.route_url('pullrequest_show',
1472 repo_name=pr_target_repo.repo_name,
1472 repo_name=pr_target_repo.repo_name,
1473 pull_request_id=pull_request_obj.pull_request_id,)
1473 pull_request_id=pull_request_obj.pull_request_id,)
1474
1474
1475 # set some variables for email notification
1475 # set some variables for email notification
1476 pr_target_repo_url = h.route_url(
1476 pr_target_repo_url = h.route_url(
1477 'repo_summary', repo_name=pr_target_repo.repo_name)
1477 'repo_summary', repo_name=pr_target_repo.repo_name)
1478
1478
1479 pr_source_repo_url = h.route_url(
1479 pr_source_repo_url = h.route_url(
1480 'repo_summary', repo_name=pr_source_repo.repo_name)
1480 'repo_summary', repo_name=pr_source_repo.repo_name)
1481
1481
1482 # pull request specifics
1482 # pull request specifics
1483 pull_request_commits = [
1483 pull_request_commits = [
1484 (x.raw_id, x.message)
1484 (x.raw_id, x.message)
1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1486
1486
1487 current_rhodecode_user = user
1487 current_rhodecode_user = user
1488 kwargs = {
1488 kwargs = {
1489 'user': current_rhodecode_user,
1489 'user': current_rhodecode_user,
1490 'pull_request_author': pull_request.author,
1490 'pull_request_author': pull_request.author,
1491 'pull_request': pull_request_obj,
1491 'pull_request': pull_request_obj,
1492 'pull_request_commits': pull_request_commits,
1492 'pull_request_commits': pull_request_commits,
1493
1493
1494 'pull_request_target_repo': pr_target_repo,
1494 'pull_request_target_repo': pr_target_repo,
1495 'pull_request_target_repo_url': pr_target_repo_url,
1495 'pull_request_target_repo_url': pr_target_repo_url,
1496
1496
1497 'pull_request_source_repo': pr_source_repo,
1497 'pull_request_source_repo': pr_source_repo,
1498 'pull_request_source_repo_url': pr_source_repo_url,
1498 'pull_request_source_repo_url': pr_source_repo_url,
1499
1499
1500 'pull_request_url': pr_url,
1500 'pull_request_url': pr_url,
1501 'thread_ids': [pr_url],
1501 'thread_ids': [pr_url],
1502 'user_role': role
1502 'user_role': role
1503 }
1503 }
1504
1504
1505 # pre-generate the subject for notification itself
1506 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1507 notification_type, **kwargs)
1508
1509 # create notification objects, and emails
1505 # create notification objects, and emails
1510 NotificationModel().create(
1506 NotificationModel().create(
1511 created_by=current_rhodecode_user,
1507 created_by=current_rhodecode_user,
1512 notification_subject=subject,
1508 notification_subject='', # Filled in based on the notification_type
1513 notification_body=body_plaintext,
1509 notification_body='', # Filled in based on the notification_type
1514 notification_type=notification_type,
1510 notification_type=notification_type,
1515 recipients=recipients,
1511 recipients=recipients,
1516 email_kwargs=kwargs,
1512 email_kwargs=kwargs,
1517 )
1513 )
1518
1514
1519 def notify_reviewers(self, pull_request, reviewers_ids, user):
1515 def notify_reviewers(self, pull_request, reviewers_ids, user):
1520 return self._notify_reviewers(pull_request, reviewers_ids,
1516 return self._notify_reviewers(pull_request, reviewers_ids,
1521 PullRequestReviewers.ROLE_REVIEWER, user)
1517 PullRequestReviewers.ROLE_REVIEWER, user)
1522
1518
1523 def notify_observers(self, pull_request, observers_ids, user):
1519 def notify_observers(self, pull_request, observers_ids, user):
1524 return self._notify_reviewers(pull_request, observers_ids,
1520 return self._notify_reviewers(pull_request, observers_ids,
1525 PullRequestReviewers.ROLE_OBSERVER, user)
1521 PullRequestReviewers.ROLE_OBSERVER, user)
1526
1522
1527 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1523 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1528 commit_changes, file_changes):
1524 commit_changes, file_changes):
1529
1525
1530 updating_user_id = updating_user.user_id
1526 updating_user_id = updating_user.user_id
1531 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1527 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1532 # NOTE(marcink): send notification to all other users except to
1528 # NOTE(marcink): send notification to all other users except to
1533 # person who updated the PR
1529 # person who updated the PR
1534 recipients = reviewers.difference(set([updating_user_id]))
1530 recipients = reviewers.difference(set([updating_user_id]))
1535
1531
1536 log.debug('Notify following recipients about pull-request update %s', recipients)
1532 log.debug('Notify following recipients about pull-request update %s', recipients)
1537
1533
1538 pull_request_obj = pull_request
1534 pull_request_obj = pull_request
1539
1535
1540 # send email about the update
1536 # send email about the update
1541 changed_files = (
1537 changed_files = (
1542 file_changes.added + file_changes.modified + file_changes.removed)
1538 file_changes.added + file_changes.modified + file_changes.removed)
1543
1539
1544 pr_source_repo = pull_request_obj.source_repo
1540 pr_source_repo = pull_request_obj.source_repo
1545 pr_target_repo = pull_request_obj.target_repo
1541 pr_target_repo = pull_request_obj.target_repo
1546
1542
1547 pr_url = h.route_url('pullrequest_show',
1543 pr_url = h.route_url('pullrequest_show',
1548 repo_name=pr_target_repo.repo_name,
1544 repo_name=pr_target_repo.repo_name,
1549 pull_request_id=pull_request_obj.pull_request_id,)
1545 pull_request_id=pull_request_obj.pull_request_id,)
1550
1546
1551 # set some variables for email notification
1547 # set some variables for email notification
1552 pr_target_repo_url = h.route_url(
1548 pr_target_repo_url = h.route_url(
1553 'repo_summary', repo_name=pr_target_repo.repo_name)
1549 'repo_summary', repo_name=pr_target_repo.repo_name)
1554
1550
1555 pr_source_repo_url = h.route_url(
1551 pr_source_repo_url = h.route_url(
1556 'repo_summary', repo_name=pr_source_repo.repo_name)
1552 'repo_summary', repo_name=pr_source_repo.repo_name)
1557
1553
1558 email_kwargs = {
1554 email_kwargs = {
1559 'date': datetime.datetime.now(),
1555 'date': datetime.datetime.now(),
1560 'updating_user': updating_user,
1556 'updating_user': updating_user,
1561
1557
1562 'pull_request': pull_request_obj,
1558 'pull_request': pull_request_obj,
1563
1559
1564 'pull_request_target_repo': pr_target_repo,
1560 'pull_request_target_repo': pr_target_repo,
1565 'pull_request_target_repo_url': pr_target_repo_url,
1561 'pull_request_target_repo_url': pr_target_repo_url,
1566
1562
1567 'pull_request_source_repo': pr_source_repo,
1563 'pull_request_source_repo': pr_source_repo,
1568 'pull_request_source_repo_url': pr_source_repo_url,
1564 'pull_request_source_repo_url': pr_source_repo_url,
1569
1565
1570 'pull_request_url': pr_url,
1566 'pull_request_url': pr_url,
1571
1567
1572 'ancestor_commit_id': ancestor_commit_id,
1568 'ancestor_commit_id': ancestor_commit_id,
1573 'added_commits': commit_changes.added,
1569 'added_commits': commit_changes.added,
1574 'removed_commits': commit_changes.removed,
1570 'removed_commits': commit_changes.removed,
1575 'changed_files': changed_files,
1571 'changed_files': changed_files,
1576 'added_files': file_changes.added,
1572 'added_files': file_changes.added,
1577 'modified_files': file_changes.modified,
1573 'modified_files': file_changes.modified,
1578 'removed_files': file_changes.removed,
1574 'removed_files': file_changes.removed,
1579 'thread_ids': [pr_url],
1575 'thread_ids': [pr_url],
1580 }
1576 }
1581
1577
1582 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1583 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1584
1585 # create notification objects, and emails
1578 # create notification objects, and emails
1586 NotificationModel().create(
1579 NotificationModel().create(
1587 created_by=updating_user,
1580 created_by=updating_user,
1588 notification_subject=subject,
1581 notification_subject='', # Filled in based on the notification_type
1589 notification_body=body_plaintext,
1582 notification_body='', # Filled in based on the notification_type
1590 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1583 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1591 recipients=recipients,
1584 recipients=recipients,
1592 email_kwargs=email_kwargs,
1585 email_kwargs=email_kwargs,
1593 )
1586 )
1594
1587
1595 def delete(self, pull_request, user=None):
1588 def delete(self, pull_request, user=None):
1596 if not user:
1589 if not user:
1597 user = getattr(get_current_rhodecode_user(), 'username', None)
1590 user = getattr(get_current_rhodecode_user(), 'username', None)
1598
1591
1599 pull_request = self.__get_pull_request(pull_request)
1592 pull_request = self.__get_pull_request(pull_request)
1600 old_data = pull_request.get_api_data(with_merge_state=False)
1593 old_data = pull_request.get_api_data(with_merge_state=False)
1601 self._cleanup_merge_workspace(pull_request)
1594 self._cleanup_merge_workspace(pull_request)
1602 self._log_audit_action(
1595 self._log_audit_action(
1603 'repo.pull_request.delete', {'old_data': old_data},
1596 'repo.pull_request.delete', {'old_data': old_data},
1604 user, pull_request)
1597 user, pull_request)
1605 Session().delete(pull_request)
1598 Session().delete(pull_request)
1606
1599
1607 def close_pull_request(self, pull_request, user):
1600 def close_pull_request(self, pull_request, user):
1608 pull_request = self.__get_pull_request(pull_request)
1601 pull_request = self.__get_pull_request(pull_request)
1609 self._cleanup_merge_workspace(pull_request)
1602 self._cleanup_merge_workspace(pull_request)
1610 pull_request.status = PullRequest.STATUS_CLOSED
1603 pull_request.status = PullRequest.STATUS_CLOSED
1611 pull_request.updated_on = datetime.datetime.now()
1604 pull_request.updated_on = datetime.datetime.now()
1612 Session().add(pull_request)
1605 Session().add(pull_request)
1613 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1606 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1614
1607
1615 pr_data = pull_request.get_api_data(with_merge_state=False)
1608 pr_data = pull_request.get_api_data(with_merge_state=False)
1616 self._log_audit_action(
1609 self._log_audit_action(
1617 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1610 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1618
1611
1619 def close_pull_request_with_comment(
1612 def close_pull_request_with_comment(
1620 self, pull_request, user, repo, message=None, auth_user=None):
1613 self, pull_request, user, repo, message=None, auth_user=None):
1621
1614
1622 pull_request_review_status = pull_request.calculated_review_status()
1615 pull_request_review_status = pull_request.calculated_review_status()
1623
1616
1624 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1617 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1625 # approved only if we have voting consent
1618 # approved only if we have voting consent
1626 status = ChangesetStatus.STATUS_APPROVED
1619 status = ChangesetStatus.STATUS_APPROVED
1627 else:
1620 else:
1628 status = ChangesetStatus.STATUS_REJECTED
1621 status = ChangesetStatus.STATUS_REJECTED
1629 status_lbl = ChangesetStatus.get_status_lbl(status)
1622 status_lbl = ChangesetStatus.get_status_lbl(status)
1630
1623
1631 default_message = (
1624 default_message = (
1632 'Closing with status change {transition_icon} {status}.'
1625 'Closing with status change {transition_icon} {status}.'
1633 ).format(transition_icon='>', status=status_lbl)
1626 ).format(transition_icon='>', status=status_lbl)
1634 text = message or default_message
1627 text = message or default_message
1635
1628
1636 # create a comment, and link it to new status
1629 # create a comment, and link it to new status
1637 comment = CommentsModel().create(
1630 comment = CommentsModel().create(
1638 text=text,
1631 text=text,
1639 repo=repo.repo_id,
1632 repo=repo.repo_id,
1640 user=user.user_id,
1633 user=user.user_id,
1641 pull_request=pull_request.pull_request_id,
1634 pull_request=pull_request.pull_request_id,
1642 status_change=status_lbl,
1635 status_change=status_lbl,
1643 status_change_type=status,
1636 status_change_type=status,
1644 closing_pr=True,
1637 closing_pr=True,
1645 auth_user=auth_user,
1638 auth_user=auth_user,
1646 )
1639 )
1647
1640
1648 # calculate old status before we change it
1641 # calculate old status before we change it
1649 old_calculated_status = pull_request.calculated_review_status()
1642 old_calculated_status = pull_request.calculated_review_status()
1650 ChangesetStatusModel().set_status(
1643 ChangesetStatusModel().set_status(
1651 repo.repo_id,
1644 repo.repo_id,
1652 status,
1645 status,
1653 user.user_id,
1646 user.user_id,
1654 comment=comment,
1647 comment=comment,
1655 pull_request=pull_request.pull_request_id
1648 pull_request=pull_request.pull_request_id
1656 )
1649 )
1657
1650
1658 Session().flush()
1651 Session().flush()
1659
1652
1660 self.trigger_pull_request_hook(pull_request, user, 'comment',
1653 self.trigger_pull_request_hook(pull_request, user, 'comment',
1661 data={'comment': comment})
1654 data={'comment': comment})
1662
1655
1663 # we now calculate the status of pull request again, and based on that
1656 # we now calculate the status of pull request again, and based on that
1664 # calculation trigger status change. This might happen in cases
1657 # calculation trigger status change. This might happen in cases
1665 # that non-reviewer admin closes a pr, which means his vote doesn't
1658 # that non-reviewer admin closes a pr, which means his vote doesn't
1666 # change the status, while if he's a reviewer this might change it.
1659 # change the status, while if he's a reviewer this might change it.
1667 calculated_status = pull_request.calculated_review_status()
1660 calculated_status = pull_request.calculated_review_status()
1668 if old_calculated_status != calculated_status:
1661 if old_calculated_status != calculated_status:
1669 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1662 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1670 data={'status': calculated_status})
1663 data={'status': calculated_status})
1671
1664
1672 # finally close the PR
1665 # finally close the PR
1673 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1666 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1674
1667
1675 return comment, status
1668 return comment, status
1676
1669
1677 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1670 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1678 _ = translator or get_current_request().translate
1671 _ = translator or get_current_request().translate
1679
1672
1680 if not self._is_merge_enabled(pull_request):
1673 if not self._is_merge_enabled(pull_request):
1681 return None, False, _('Server-side pull request merging is disabled.')
1674 return None, False, _('Server-side pull request merging is disabled.')
1682
1675
1683 if pull_request.is_closed():
1676 if pull_request.is_closed():
1684 return None, False, _('This pull request is closed.')
1677 return None, False, _('This pull request is closed.')
1685
1678
1686 merge_possible, msg = self._check_repo_requirements(
1679 merge_possible, msg = self._check_repo_requirements(
1687 target=pull_request.target_repo, source=pull_request.source_repo,
1680 target=pull_request.target_repo, source=pull_request.source_repo,
1688 translator=_)
1681 translator=_)
1689 if not merge_possible:
1682 if not merge_possible:
1690 return None, merge_possible, msg
1683 return None, merge_possible, msg
1691
1684
1692 try:
1685 try:
1693 merge_response = self._try_merge(
1686 merge_response = self._try_merge(
1694 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1687 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1695 log.debug("Merge response: %s", merge_response)
1688 log.debug("Merge response: %s", merge_response)
1696 return merge_response, merge_response.possible, merge_response.merge_status_message
1689 return merge_response, merge_response.possible, merge_response.merge_status_message
1697 except NotImplementedError:
1690 except NotImplementedError:
1698 return None, False, _('Pull request merging is not supported.')
1691 return None, False, _('Pull request merging is not supported.')
1699
1692
1700 def _check_repo_requirements(self, target, source, translator):
1693 def _check_repo_requirements(self, target, source, translator):
1701 """
1694 """
1702 Check if `target` and `source` have compatible requirements.
1695 Check if `target` and `source` have compatible requirements.
1703
1696
1704 Currently this is just checking for largefiles.
1697 Currently this is just checking for largefiles.
1705 """
1698 """
1706 _ = translator
1699 _ = translator
1707 target_has_largefiles = self._has_largefiles(target)
1700 target_has_largefiles = self._has_largefiles(target)
1708 source_has_largefiles = self._has_largefiles(source)
1701 source_has_largefiles = self._has_largefiles(source)
1709 merge_possible = True
1702 merge_possible = True
1710 message = u''
1703 message = u''
1711
1704
1712 if target_has_largefiles != source_has_largefiles:
1705 if target_has_largefiles != source_has_largefiles:
1713 merge_possible = False
1706 merge_possible = False
1714 if source_has_largefiles:
1707 if source_has_largefiles:
1715 message = _(
1708 message = _(
1716 'Target repository large files support is disabled.')
1709 'Target repository large files support is disabled.')
1717 else:
1710 else:
1718 message = _(
1711 message = _(
1719 'Source repository large files support is disabled.')
1712 'Source repository large files support is disabled.')
1720
1713
1721 return merge_possible, message
1714 return merge_possible, message
1722
1715
1723 def _has_largefiles(self, repo):
1716 def _has_largefiles(self, repo):
1724 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1717 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1725 'extensions', 'largefiles')
1718 'extensions', 'largefiles')
1726 return largefiles_ui and largefiles_ui[0].active
1719 return largefiles_ui and largefiles_ui[0].active
1727
1720
1728 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1721 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1729 """
1722 """
1730 Try to merge the pull request and return the merge status.
1723 Try to merge the pull request and return the merge status.
1731 """
1724 """
1732 log.debug(
1725 log.debug(
1733 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1726 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1734 pull_request.pull_request_id, force_shadow_repo_refresh)
1727 pull_request.pull_request_id, force_shadow_repo_refresh)
1735 target_vcs = pull_request.target_repo.scm_instance()
1728 target_vcs = pull_request.target_repo.scm_instance()
1736 # Refresh the target reference.
1729 # Refresh the target reference.
1737 try:
1730 try:
1738 target_ref = self._refresh_reference(
1731 target_ref = self._refresh_reference(
1739 pull_request.target_ref_parts, target_vcs)
1732 pull_request.target_ref_parts, target_vcs)
1740 except CommitDoesNotExistError:
1733 except CommitDoesNotExistError:
1741 merge_state = MergeResponse(
1734 merge_state = MergeResponse(
1742 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1735 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1743 metadata={'target_ref': pull_request.target_ref_parts})
1736 metadata={'target_ref': pull_request.target_ref_parts})
1744 return merge_state
1737 return merge_state
1745
1738
1746 target_locked = pull_request.target_repo.locked
1739 target_locked = pull_request.target_repo.locked
1747 if target_locked and target_locked[0]:
1740 if target_locked and target_locked[0]:
1748 locked_by = 'user:{}'.format(target_locked[0])
1741 locked_by = 'user:{}'.format(target_locked[0])
1749 log.debug("The target repository is locked by %s.", locked_by)
1742 log.debug("The target repository is locked by %s.", locked_by)
1750 merge_state = MergeResponse(
1743 merge_state = MergeResponse(
1751 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1744 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1752 metadata={'locked_by': locked_by})
1745 metadata={'locked_by': locked_by})
1753 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1746 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1754 pull_request, target_ref):
1747 pull_request, target_ref):
1755 log.debug("Refreshing the merge status of the repository.")
1748 log.debug("Refreshing the merge status of the repository.")
1756 merge_state = self._refresh_merge_state(
1749 merge_state = self._refresh_merge_state(
1757 pull_request, target_vcs, target_ref)
1750 pull_request, target_vcs, target_ref)
1758 else:
1751 else:
1759 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1752 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1760 metadata = {
1753 metadata = {
1761 'unresolved_files': '',
1754 'unresolved_files': '',
1762 'target_ref': pull_request.target_ref_parts,
1755 'target_ref': pull_request.target_ref_parts,
1763 'source_ref': pull_request.source_ref_parts,
1756 'source_ref': pull_request.source_ref_parts,
1764 }
1757 }
1765 if pull_request.last_merge_metadata:
1758 if pull_request.last_merge_metadata:
1766 metadata.update(pull_request.last_merge_metadata_parsed)
1759 metadata.update(pull_request.last_merge_metadata_parsed)
1767
1760
1768 if not possible and target_ref.type == 'branch':
1761 if not possible and target_ref.type == 'branch':
1769 # NOTE(marcink): case for mercurial multiple heads on branch
1762 # NOTE(marcink): case for mercurial multiple heads on branch
1770 heads = target_vcs._heads(target_ref.name)
1763 heads = target_vcs._heads(target_ref.name)
1771 if len(heads) != 1:
1764 if len(heads) != 1:
1772 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1765 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1773 metadata.update({
1766 metadata.update({
1774 'heads': heads
1767 'heads': heads
1775 })
1768 })
1776
1769
1777 merge_state = MergeResponse(
1770 merge_state = MergeResponse(
1778 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1771 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1779
1772
1780 return merge_state
1773 return merge_state
1781
1774
1782 def _refresh_reference(self, reference, vcs_repository):
1775 def _refresh_reference(self, reference, vcs_repository):
1783 if reference.type in self.UPDATABLE_REF_TYPES:
1776 if reference.type in self.UPDATABLE_REF_TYPES:
1784 name_or_id = reference.name
1777 name_or_id = reference.name
1785 else:
1778 else:
1786 name_or_id = reference.commit_id
1779 name_or_id = reference.commit_id
1787
1780
1788 refreshed_commit = vcs_repository.get_commit(name_or_id)
1781 refreshed_commit = vcs_repository.get_commit(name_or_id)
1789 refreshed_reference = Reference(
1782 refreshed_reference = Reference(
1790 reference.type, reference.name, refreshed_commit.raw_id)
1783 reference.type, reference.name, refreshed_commit.raw_id)
1791 return refreshed_reference
1784 return refreshed_reference
1792
1785
1793 def _needs_merge_state_refresh(self, pull_request, target_reference):
1786 def _needs_merge_state_refresh(self, pull_request, target_reference):
1794 return not(
1787 return not(
1795 pull_request.revisions and
1788 pull_request.revisions and
1796 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1789 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1797 target_reference.commit_id == pull_request._last_merge_target_rev)
1790 target_reference.commit_id == pull_request._last_merge_target_rev)
1798
1791
1799 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1792 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1800 workspace_id = self._workspace_id(pull_request)
1793 workspace_id = self._workspace_id(pull_request)
1801 source_vcs = pull_request.source_repo.scm_instance()
1794 source_vcs = pull_request.source_repo.scm_instance()
1802 repo_id = pull_request.target_repo.repo_id
1795 repo_id = pull_request.target_repo.repo_id
1803 use_rebase = self._use_rebase_for_merging(pull_request)
1796 use_rebase = self._use_rebase_for_merging(pull_request)
1804 close_branch = self._close_branch_before_merging(pull_request)
1797 close_branch = self._close_branch_before_merging(pull_request)
1805 merge_state = target_vcs.merge(
1798 merge_state = target_vcs.merge(
1806 repo_id, workspace_id,
1799 repo_id, workspace_id,
1807 target_reference, source_vcs, pull_request.source_ref_parts,
1800 target_reference, source_vcs, pull_request.source_ref_parts,
1808 dry_run=True, use_rebase=use_rebase,
1801 dry_run=True, use_rebase=use_rebase,
1809 close_branch=close_branch)
1802 close_branch=close_branch)
1810
1803
1811 # Do not store the response if there was an unknown error.
1804 # Do not store the response if there was an unknown error.
1812 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1805 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1813 pull_request._last_merge_source_rev = \
1806 pull_request._last_merge_source_rev = \
1814 pull_request.source_ref_parts.commit_id
1807 pull_request.source_ref_parts.commit_id
1815 pull_request._last_merge_target_rev = target_reference.commit_id
1808 pull_request._last_merge_target_rev = target_reference.commit_id
1816 pull_request.last_merge_status = merge_state.failure_reason
1809 pull_request.last_merge_status = merge_state.failure_reason
1817 pull_request.last_merge_metadata = merge_state.metadata
1810 pull_request.last_merge_metadata = merge_state.metadata
1818
1811
1819 pull_request.shadow_merge_ref = merge_state.merge_ref
1812 pull_request.shadow_merge_ref = merge_state.merge_ref
1820 Session().add(pull_request)
1813 Session().add(pull_request)
1821 Session().commit()
1814 Session().commit()
1822
1815
1823 return merge_state
1816 return merge_state
1824
1817
1825 def _workspace_id(self, pull_request):
1818 def _workspace_id(self, pull_request):
1826 workspace_id = 'pr-%s' % pull_request.pull_request_id
1819 workspace_id = 'pr-%s' % pull_request.pull_request_id
1827 return workspace_id
1820 return workspace_id
1828
1821
1829 def generate_repo_data(self, repo, commit_id=None, branch=None,
1822 def generate_repo_data(self, repo, commit_id=None, branch=None,
1830 bookmark=None, translator=None):
1823 bookmark=None, translator=None):
1831 from rhodecode.model.repo import RepoModel
1824 from rhodecode.model.repo import RepoModel
1832
1825
1833 all_refs, selected_ref = \
1826 all_refs, selected_ref = \
1834 self._get_repo_pullrequest_sources(
1827 self._get_repo_pullrequest_sources(
1835 repo.scm_instance(), commit_id=commit_id,
1828 repo.scm_instance(), commit_id=commit_id,
1836 branch=branch, bookmark=bookmark, translator=translator)
1829 branch=branch, bookmark=bookmark, translator=translator)
1837
1830
1838 refs_select2 = []
1831 refs_select2 = []
1839 for element in all_refs:
1832 for element in all_refs:
1840 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1833 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1841 refs_select2.append({'text': element[1], 'children': children})
1834 refs_select2.append({'text': element[1], 'children': children})
1842
1835
1843 return {
1836 return {
1844 'user': {
1837 'user': {
1845 'user_id': repo.user.user_id,
1838 'user_id': repo.user.user_id,
1846 'username': repo.user.username,
1839 'username': repo.user.username,
1847 'firstname': repo.user.first_name,
1840 'firstname': repo.user.first_name,
1848 'lastname': repo.user.last_name,
1841 'lastname': repo.user.last_name,
1849 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1842 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1850 },
1843 },
1851 'name': repo.repo_name,
1844 'name': repo.repo_name,
1852 'link': RepoModel().get_url(repo),
1845 'link': RepoModel().get_url(repo),
1853 'description': h.chop_at_smart(repo.description_safe, '\n'),
1846 'description': h.chop_at_smart(repo.description_safe, '\n'),
1854 'refs': {
1847 'refs': {
1855 'all_refs': all_refs,
1848 'all_refs': all_refs,
1856 'selected_ref': selected_ref,
1849 'selected_ref': selected_ref,
1857 'select2_refs': refs_select2
1850 'select2_refs': refs_select2
1858 }
1851 }
1859 }
1852 }
1860
1853
1861 def generate_pullrequest_title(self, source, source_ref, target):
1854 def generate_pullrequest_title(self, source, source_ref, target):
1862 return u'{source}#{at_ref} to {target}'.format(
1855 return u'{source}#{at_ref} to {target}'.format(
1863 source=source,
1856 source=source,
1864 at_ref=source_ref,
1857 at_ref=source_ref,
1865 target=target,
1858 target=target,
1866 )
1859 )
1867
1860
1868 def _cleanup_merge_workspace(self, pull_request):
1861 def _cleanup_merge_workspace(self, pull_request):
1869 # Merging related cleanup
1862 # Merging related cleanup
1870 repo_id = pull_request.target_repo.repo_id
1863 repo_id = pull_request.target_repo.repo_id
1871 target_scm = pull_request.target_repo.scm_instance()
1864 target_scm = pull_request.target_repo.scm_instance()
1872 workspace_id = self._workspace_id(pull_request)
1865 workspace_id = self._workspace_id(pull_request)
1873
1866
1874 try:
1867 try:
1875 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1868 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1876 except NotImplementedError:
1869 except NotImplementedError:
1877 pass
1870 pass
1878
1871
1879 def _get_repo_pullrequest_sources(
1872 def _get_repo_pullrequest_sources(
1880 self, repo, commit_id=None, branch=None, bookmark=None,
1873 self, repo, commit_id=None, branch=None, bookmark=None,
1881 translator=None):
1874 translator=None):
1882 """
1875 """
1883 Return a structure with repo's interesting commits, suitable for
1876 Return a structure with repo's interesting commits, suitable for
1884 the selectors in pullrequest controller
1877 the selectors in pullrequest controller
1885
1878
1886 :param commit_id: a commit that must be in the list somehow
1879 :param commit_id: a commit that must be in the list somehow
1887 and selected by default
1880 and selected by default
1888 :param branch: a branch that must be in the list and selected
1881 :param branch: a branch that must be in the list and selected
1889 by default - even if closed
1882 by default - even if closed
1890 :param bookmark: a bookmark that must be in the list and selected
1883 :param bookmark: a bookmark that must be in the list and selected
1891 """
1884 """
1892 _ = translator or get_current_request().translate
1885 _ = translator or get_current_request().translate
1893
1886
1894 commit_id = safe_str(commit_id) if commit_id else None
1887 commit_id = safe_str(commit_id) if commit_id else None
1895 branch = safe_unicode(branch) if branch else None
1888 branch = safe_unicode(branch) if branch else None
1896 bookmark = safe_unicode(bookmark) if bookmark else None
1889 bookmark = safe_unicode(bookmark) if bookmark else None
1897
1890
1898 selected = None
1891 selected = None
1899
1892
1900 # order matters: first source that has commit_id in it will be selected
1893 # order matters: first source that has commit_id in it will be selected
1901 sources = []
1894 sources = []
1902 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1895 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1903 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1896 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1904
1897
1905 if commit_id:
1898 if commit_id:
1906 ref_commit = (h.short_id(commit_id), commit_id)
1899 ref_commit = (h.short_id(commit_id), commit_id)
1907 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1900 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1908
1901
1909 sources.append(
1902 sources.append(
1910 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1903 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1911 )
1904 )
1912
1905
1913 groups = []
1906 groups = []
1914
1907
1915 for group_key, ref_list, group_name, match in sources:
1908 for group_key, ref_list, group_name, match in sources:
1916 group_refs = []
1909 group_refs = []
1917 for ref_name, ref_id in ref_list:
1910 for ref_name, ref_id in ref_list:
1918 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1911 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1919 group_refs.append((ref_key, ref_name))
1912 group_refs.append((ref_key, ref_name))
1920
1913
1921 if not selected:
1914 if not selected:
1922 if set([commit_id, match]) & set([ref_id, ref_name]):
1915 if set([commit_id, match]) & set([ref_id, ref_name]):
1923 selected = ref_key
1916 selected = ref_key
1924
1917
1925 if group_refs:
1918 if group_refs:
1926 groups.append((group_refs, group_name))
1919 groups.append((group_refs, group_name))
1927
1920
1928 if not selected:
1921 if not selected:
1929 ref = commit_id or branch or bookmark
1922 ref = commit_id or branch or bookmark
1930 if ref:
1923 if ref:
1931 raise CommitDoesNotExistError(
1924 raise CommitDoesNotExistError(
1932 u'No commit refs could be found matching: {}'.format(ref))
1925 u'No commit refs could be found matching: {}'.format(ref))
1933 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1926 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1934 selected = u'branch:{}:{}'.format(
1927 selected = u'branch:{}:{}'.format(
1935 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1928 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1936 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1929 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1937 )
1930 )
1938 elif repo.commit_ids:
1931 elif repo.commit_ids:
1939 # make the user select in this case
1932 # make the user select in this case
1940 selected = None
1933 selected = None
1941 else:
1934 else:
1942 raise EmptyRepositoryError()
1935 raise EmptyRepositoryError()
1943 return groups, selected
1936 return groups, selected
1944
1937
1945 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1938 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1946 hide_whitespace_changes, diff_context):
1939 hide_whitespace_changes, diff_context):
1947
1940
1948 return self._get_diff_from_pr_or_version(
1941 return self._get_diff_from_pr_or_version(
1949 source_repo, source_ref_id, target_ref_id,
1942 source_repo, source_ref_id, target_ref_id,
1950 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1943 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1951
1944
1952 def _get_diff_from_pr_or_version(
1945 def _get_diff_from_pr_or_version(
1953 self, source_repo, source_ref_id, target_ref_id,
1946 self, source_repo, source_ref_id, target_ref_id,
1954 hide_whitespace_changes, diff_context):
1947 hide_whitespace_changes, diff_context):
1955
1948
1956 target_commit = source_repo.get_commit(
1949 target_commit = source_repo.get_commit(
1957 commit_id=safe_str(target_ref_id))
1950 commit_id=safe_str(target_ref_id))
1958 source_commit = source_repo.get_commit(
1951 source_commit = source_repo.get_commit(
1959 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1952 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1960 if isinstance(source_repo, Repository):
1953 if isinstance(source_repo, Repository):
1961 vcs_repo = source_repo.scm_instance()
1954 vcs_repo = source_repo.scm_instance()
1962 else:
1955 else:
1963 vcs_repo = source_repo
1956 vcs_repo = source_repo
1964
1957
1965 # TODO: johbo: In the context of an update, we cannot reach
1958 # TODO: johbo: In the context of an update, we cannot reach
1966 # the old commit anymore with our normal mechanisms. It needs
1959 # the old commit anymore with our normal mechanisms. It needs
1967 # some sort of special support in the vcs layer to avoid this
1960 # some sort of special support in the vcs layer to avoid this
1968 # workaround.
1961 # workaround.
1969 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1962 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1970 vcs_repo.alias == 'git'):
1963 vcs_repo.alias == 'git'):
1971 source_commit.raw_id = safe_str(source_ref_id)
1964 source_commit.raw_id = safe_str(source_ref_id)
1972
1965
1973 log.debug('calculating diff between '
1966 log.debug('calculating diff between '
1974 'source_ref:%s and target_ref:%s for repo `%s`',
1967 'source_ref:%s and target_ref:%s for repo `%s`',
1975 target_ref_id, source_ref_id,
1968 target_ref_id, source_ref_id,
1976 safe_unicode(vcs_repo.path))
1969 safe_unicode(vcs_repo.path))
1977
1970
1978 vcs_diff = vcs_repo.get_diff(
1971 vcs_diff = vcs_repo.get_diff(
1979 commit1=target_commit, commit2=source_commit,
1972 commit1=target_commit, commit2=source_commit,
1980 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1973 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1981 return vcs_diff
1974 return vcs_diff
1982
1975
1983 def _is_merge_enabled(self, pull_request):
1976 def _is_merge_enabled(self, pull_request):
1984 return self._get_general_setting(
1977 return self._get_general_setting(
1985 pull_request, 'rhodecode_pr_merge_enabled')
1978 pull_request, 'rhodecode_pr_merge_enabled')
1986
1979
1987 def _use_rebase_for_merging(self, pull_request):
1980 def _use_rebase_for_merging(self, pull_request):
1988 repo_type = pull_request.target_repo.repo_type
1981 repo_type = pull_request.target_repo.repo_type
1989 if repo_type == 'hg':
1982 if repo_type == 'hg':
1990 return self._get_general_setting(
1983 return self._get_general_setting(
1991 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1984 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1992 elif repo_type == 'git':
1985 elif repo_type == 'git':
1993 return self._get_general_setting(
1986 return self._get_general_setting(
1994 pull_request, 'rhodecode_git_use_rebase_for_merging')
1987 pull_request, 'rhodecode_git_use_rebase_for_merging')
1995
1988
1996 return False
1989 return False
1997
1990
1998 def _user_name_for_merging(self, pull_request, user):
1991 def _user_name_for_merging(self, pull_request, user):
1999 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1992 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2000 if env_user_name_attr and hasattr(user, env_user_name_attr):
1993 if env_user_name_attr and hasattr(user, env_user_name_attr):
2001 user_name_attr = env_user_name_attr
1994 user_name_attr = env_user_name_attr
2002 else:
1995 else:
2003 user_name_attr = 'short_contact'
1996 user_name_attr = 'short_contact'
2004
1997
2005 user_name = getattr(user, user_name_attr)
1998 user_name = getattr(user, user_name_attr)
2006 return user_name
1999 return user_name
2007
2000
2008 def _close_branch_before_merging(self, pull_request):
2001 def _close_branch_before_merging(self, pull_request):
2009 repo_type = pull_request.target_repo.repo_type
2002 repo_type = pull_request.target_repo.repo_type
2010 if repo_type == 'hg':
2003 if repo_type == 'hg':
2011 return self._get_general_setting(
2004 return self._get_general_setting(
2012 pull_request, 'rhodecode_hg_close_branch_before_merging')
2005 pull_request, 'rhodecode_hg_close_branch_before_merging')
2013 elif repo_type == 'git':
2006 elif repo_type == 'git':
2014 return self._get_general_setting(
2007 return self._get_general_setting(
2015 pull_request, 'rhodecode_git_close_branch_before_merging')
2008 pull_request, 'rhodecode_git_close_branch_before_merging')
2016
2009
2017 return False
2010 return False
2018
2011
2019 def _get_general_setting(self, pull_request, settings_key, default=False):
2012 def _get_general_setting(self, pull_request, settings_key, default=False):
2020 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2013 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2021 settings = settings_model.get_general_settings()
2014 settings = settings_model.get_general_settings()
2022 return settings.get(settings_key, default)
2015 return settings.get(settings_key, default)
2023
2016
2024 def _log_audit_action(self, action, action_data, user, pull_request):
2017 def _log_audit_action(self, action, action_data, user, pull_request):
2025 audit_logger.store(
2018 audit_logger.store(
2026 action=action,
2019 action=action,
2027 action_data=action_data,
2020 action_data=action_data,
2028 user=user,
2021 user=user,
2029 repo=pull_request.target_repo)
2022 repo=pull_request.target_repo)
2030
2023
2031 def get_reviewer_functions(self):
2024 def get_reviewer_functions(self):
2032 """
2025 """
2033 Fetches functions for validation and fetching default reviewers.
2026 Fetches functions for validation and fetching default reviewers.
2034 If available we use the EE package, else we fallback to CE
2027 If available we use the EE package, else we fallback to CE
2035 package functions
2028 package functions
2036 """
2029 """
2037 try:
2030 try:
2038 from rc_reviewers.utils import get_default_reviewers_data
2031 from rc_reviewers.utils import get_default_reviewers_data
2039 from rc_reviewers.utils import validate_default_reviewers
2032 from rc_reviewers.utils import validate_default_reviewers
2040 from rc_reviewers.utils import validate_observers
2033 from rc_reviewers.utils import validate_observers
2041 except ImportError:
2034 except ImportError:
2042 from rhodecode.apps.repository.utils import get_default_reviewers_data
2035 from rhodecode.apps.repository.utils import get_default_reviewers_data
2043 from rhodecode.apps.repository.utils import validate_default_reviewers
2036 from rhodecode.apps.repository.utils import validate_default_reviewers
2044 from rhodecode.apps.repository.utils import validate_observers
2037 from rhodecode.apps.repository.utils import validate_observers
2045
2038
2046 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2039 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2047
2040
2048
2041
2049 class MergeCheck(object):
2042 class MergeCheck(object):
2050 """
2043 """
2051 Perform Merge Checks and returns a check object which stores information
2044 Perform Merge Checks and returns a check object which stores information
2052 about merge errors, and merge conditions
2045 about merge errors, and merge conditions
2053 """
2046 """
2054 TODO_CHECK = 'todo'
2047 TODO_CHECK = 'todo'
2055 PERM_CHECK = 'perm'
2048 PERM_CHECK = 'perm'
2056 REVIEW_CHECK = 'review'
2049 REVIEW_CHECK = 'review'
2057 MERGE_CHECK = 'merge'
2050 MERGE_CHECK = 'merge'
2058 WIP_CHECK = 'wip'
2051 WIP_CHECK = 'wip'
2059
2052
2060 def __init__(self):
2053 def __init__(self):
2061 self.review_status = None
2054 self.review_status = None
2062 self.merge_possible = None
2055 self.merge_possible = None
2063 self.merge_msg = ''
2056 self.merge_msg = ''
2064 self.merge_response = None
2057 self.merge_response = None
2065 self.failed = None
2058 self.failed = None
2066 self.errors = []
2059 self.errors = []
2067 self.error_details = OrderedDict()
2060 self.error_details = OrderedDict()
2068 self.source_commit = AttributeDict()
2061 self.source_commit = AttributeDict()
2069 self.target_commit = AttributeDict()
2062 self.target_commit = AttributeDict()
2070
2063
2071 def __repr__(self):
2064 def __repr__(self):
2072 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2065 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2073 self.merge_possible, self.failed, self.errors)
2066 self.merge_possible, self.failed, self.errors)
2074
2067
2075 def push_error(self, error_type, message, error_key, details):
2068 def push_error(self, error_type, message, error_key, details):
2076 self.failed = True
2069 self.failed = True
2077 self.errors.append([error_type, message])
2070 self.errors.append([error_type, message])
2078 self.error_details[error_key] = dict(
2071 self.error_details[error_key] = dict(
2079 details=details,
2072 details=details,
2080 error_type=error_type,
2073 error_type=error_type,
2081 message=message
2074 message=message
2082 )
2075 )
2083
2076
2084 @classmethod
2077 @classmethod
2085 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2078 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2086 force_shadow_repo_refresh=False):
2079 force_shadow_repo_refresh=False):
2087 _ = translator
2080 _ = translator
2088 merge_check = cls()
2081 merge_check = cls()
2089
2082
2090 # title has WIP:
2083 # title has WIP:
2091 if pull_request.work_in_progress:
2084 if pull_request.work_in_progress:
2092 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2085 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2093
2086
2094 msg = _('WIP marker in title prevents from accidental merge.')
2087 msg = _('WIP marker in title prevents from accidental merge.')
2095 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2088 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2096 if fail_early:
2089 if fail_early:
2097 return merge_check
2090 return merge_check
2098
2091
2099 # permissions to merge
2092 # permissions to merge
2100 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2093 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2101 if not user_allowed_to_merge:
2094 if not user_allowed_to_merge:
2102 log.debug("MergeCheck: cannot merge, approval is pending.")
2095 log.debug("MergeCheck: cannot merge, approval is pending.")
2103
2096
2104 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2097 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2105 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2098 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2106 if fail_early:
2099 if fail_early:
2107 return merge_check
2100 return merge_check
2108
2101
2109 # permission to merge into the target branch
2102 # permission to merge into the target branch
2110 target_commit_id = pull_request.target_ref_parts.commit_id
2103 target_commit_id = pull_request.target_ref_parts.commit_id
2111 if pull_request.target_ref_parts.type == 'branch':
2104 if pull_request.target_ref_parts.type == 'branch':
2112 branch_name = pull_request.target_ref_parts.name
2105 branch_name = pull_request.target_ref_parts.name
2113 else:
2106 else:
2114 # for mercurial we can always figure out the branch from the commit
2107 # for mercurial we can always figure out the branch from the commit
2115 # in case of bookmark
2108 # in case of bookmark
2116 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2109 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2117 branch_name = target_commit.branch
2110 branch_name = target_commit.branch
2118
2111
2119 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2112 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2120 pull_request.target_repo.repo_name, branch_name)
2113 pull_request.target_repo.repo_name, branch_name)
2121 if branch_perm and branch_perm == 'branch.none':
2114 if branch_perm and branch_perm == 'branch.none':
2122 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2115 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2123 branch_name, rule)
2116 branch_name, rule)
2124 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2117 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2125 if fail_early:
2118 if fail_early:
2126 return merge_check
2119 return merge_check
2127
2120
2128 # review status, must be always present
2121 # review status, must be always present
2129 review_status = pull_request.calculated_review_status()
2122 review_status = pull_request.calculated_review_status()
2130 merge_check.review_status = review_status
2123 merge_check.review_status = review_status
2131
2124
2132 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2125 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2133 if not status_approved:
2126 if not status_approved:
2134 log.debug("MergeCheck: cannot merge, approval is pending.")
2127 log.debug("MergeCheck: cannot merge, approval is pending.")
2135
2128
2136 msg = _('Pull request reviewer approval is pending.')
2129 msg = _('Pull request reviewer approval is pending.')
2137
2130
2138 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2131 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2139
2132
2140 if fail_early:
2133 if fail_early:
2141 return merge_check
2134 return merge_check
2142
2135
2143 # left over TODOs
2136 # left over TODOs
2144 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2137 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2145 if todos:
2138 if todos:
2146 log.debug("MergeCheck: cannot merge, {} "
2139 log.debug("MergeCheck: cannot merge, {} "
2147 "unresolved TODOs left.".format(len(todos)))
2140 "unresolved TODOs left.".format(len(todos)))
2148
2141
2149 if len(todos) == 1:
2142 if len(todos) == 1:
2150 msg = _('Cannot merge, {} TODO still not resolved.').format(
2143 msg = _('Cannot merge, {} TODO still not resolved.').format(
2151 len(todos))
2144 len(todos))
2152 else:
2145 else:
2153 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2146 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2154 len(todos))
2147 len(todos))
2155
2148
2156 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2149 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2157
2150
2158 if fail_early:
2151 if fail_early:
2159 return merge_check
2152 return merge_check
2160
2153
2161 # merge possible, here is the filesystem simulation + shadow repo
2154 # merge possible, here is the filesystem simulation + shadow repo
2162 merge_response, merge_status, msg = PullRequestModel().merge_status(
2155 merge_response, merge_status, msg = PullRequestModel().merge_status(
2163 pull_request, translator=translator,
2156 pull_request, translator=translator,
2164 force_shadow_repo_refresh=force_shadow_repo_refresh)
2157 force_shadow_repo_refresh=force_shadow_repo_refresh)
2165
2158
2166 merge_check.merge_possible = merge_status
2159 merge_check.merge_possible = merge_status
2167 merge_check.merge_msg = msg
2160 merge_check.merge_msg = msg
2168 merge_check.merge_response = merge_response
2161 merge_check.merge_response = merge_response
2169
2162
2170 source_ref_id = pull_request.source_ref_parts.commit_id
2163 source_ref_id = pull_request.source_ref_parts.commit_id
2171 target_ref_id = pull_request.target_ref_parts.commit_id
2164 target_ref_id = pull_request.target_ref_parts.commit_id
2172
2165
2173 try:
2166 try:
2174 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2167 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2175 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2168 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2176 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2169 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2177 merge_check.source_commit.current_raw_id = source_commit.raw_id
2170 merge_check.source_commit.current_raw_id = source_commit.raw_id
2178 merge_check.source_commit.previous_raw_id = source_ref_id
2171 merge_check.source_commit.previous_raw_id = source_ref_id
2179
2172
2180 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2173 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2181 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2174 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2182 merge_check.target_commit.current_raw_id = target_commit.raw_id
2175 merge_check.target_commit.current_raw_id = target_commit.raw_id
2183 merge_check.target_commit.previous_raw_id = target_ref_id
2176 merge_check.target_commit.previous_raw_id = target_ref_id
2184 except (SourceRefMissing, TargetRefMissing):
2177 except (SourceRefMissing, TargetRefMissing):
2185 pass
2178 pass
2186
2179
2187 if not merge_status:
2180 if not merge_status:
2188 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2181 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2189 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2182 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2190
2183
2191 if fail_early:
2184 if fail_early:
2192 return merge_check
2185 return merge_check
2193
2186
2194 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2187 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2195 return merge_check
2188 return merge_check
2196
2189
2197 @classmethod
2190 @classmethod
2198 def get_merge_conditions(cls, pull_request, translator):
2191 def get_merge_conditions(cls, pull_request, translator):
2199 _ = translator
2192 _ = translator
2200 merge_details = {}
2193 merge_details = {}
2201
2194
2202 model = PullRequestModel()
2195 model = PullRequestModel()
2203 use_rebase = model._use_rebase_for_merging(pull_request)
2196 use_rebase = model._use_rebase_for_merging(pull_request)
2204
2197
2205 if use_rebase:
2198 if use_rebase:
2206 merge_details['merge_strategy'] = dict(
2199 merge_details['merge_strategy'] = dict(
2207 details={},
2200 details={},
2208 message=_('Merge strategy: rebase')
2201 message=_('Merge strategy: rebase')
2209 )
2202 )
2210 else:
2203 else:
2211 merge_details['merge_strategy'] = dict(
2204 merge_details['merge_strategy'] = dict(
2212 details={},
2205 details={},
2213 message=_('Merge strategy: explicit merge commit')
2206 message=_('Merge strategy: explicit merge commit')
2214 )
2207 )
2215
2208
2216 close_branch = model._close_branch_before_merging(pull_request)
2209 close_branch = model._close_branch_before_merging(pull_request)
2217 if close_branch:
2210 if close_branch:
2218 repo_type = pull_request.target_repo.repo_type
2211 repo_type = pull_request.target_repo.repo_type
2219 close_msg = ''
2212 close_msg = ''
2220 if repo_type == 'hg':
2213 if repo_type == 'hg':
2221 close_msg = _('Source branch will be closed before the merge.')
2214 close_msg = _('Source branch will be closed before the merge.')
2222 elif repo_type == 'git':
2215 elif repo_type == 'git':
2223 close_msg = _('Source branch will be deleted after the merge.')
2216 close_msg = _('Source branch will be deleted after the merge.')
2224
2217
2225 merge_details['close_branch'] = dict(
2218 merge_details['close_branch'] = dict(
2226 details={},
2219 details={},
2227 message=close_msg
2220 message=close_msg
2228 )
2221 )
2229
2222
2230 return merge_details
2223 return merge_details
2231
2224
2232
2225
2233 ChangeTuple = collections.namedtuple(
2226 ChangeTuple = collections.namedtuple(
2234 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2227 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2235
2228
2236 FileChangeTuple = collections.namedtuple(
2229 FileChangeTuple = collections.namedtuple(
2237 'FileChangeTuple', ['added', 'modified', 'removed'])
2230 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,1050 +1,1047 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 users model for RhodeCode
22 users model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import datetime
27 import datetime
28 import ipaddress
28 import ipaddress
29
29
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.exc import DatabaseError
31 from sqlalchemy.exc import DatabaseError
32
32
33 from rhodecode import events
33 from rhodecode import events
34 from rhodecode.lib.user_log_filter import user_log_filter
34 from rhodecode.lib.user_log_filter import user_log_filter
35 from rhodecode.lib.utils2 import (
35 from rhodecode.lib.utils2 import (
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 AttributeDict, str2bool)
37 AttributeDict, str2bool)
38 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43 from rhodecode.model import BaseModel
43 from rhodecode.model import BaseModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
46 UserEmailMap, UserIpMap, UserLog)
46 UserEmailMap, UserIpMap, UserLog)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class UserModel(BaseModel):
54 class UserModel(BaseModel):
55 cls = User
55 cls = User
56
56
57 def get(self, user_id, cache=False):
57 def get(self, user_id, cache=False):
58 user = self.sa.query(User)
58 user = self.sa.query(User)
59 if cache:
59 if cache:
60 user = user.options(
60 user = user.options(
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 return user.get(user_id)
62 return user.get(user_id)
63
63
64 def get_user(self, user):
64 def get_user(self, user):
65 return self._get_user(user)
65 return self._get_user(user)
66
66
67 def _serialize_user(self, user):
67 def _serialize_user(self, user):
68 import rhodecode.lib.helpers as h
68 import rhodecode.lib.helpers as h
69
69
70 return {
70 return {
71 'id': user.user_id,
71 'id': user.user_id,
72 'first_name': user.first_name,
72 'first_name': user.first_name,
73 'last_name': user.last_name,
73 'last_name': user.last_name,
74 'username': user.username,
74 'username': user.username,
75 'email': user.email,
75 'email': user.email,
76 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 'profile_link': h.link_to_user(user),
78 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
79 'value': user.username,
79 'value': user.username,
80 'value_type': 'user',
80 'value_type': 'user',
81 'active': user.active,
81 'active': user.active,
82 }
82 }
83
83
84 def get_users(self, name_contains=None, limit=20, only_active=True):
84 def get_users(self, name_contains=None, limit=20, only_active=True):
85
85
86 query = self.sa.query(User)
86 query = self.sa.query(User)
87 if only_active:
87 if only_active:
88 query = query.filter(User.active == true())
88 query = query.filter(User.active == true())
89
89
90 if name_contains:
90 if name_contains:
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 query = query.filter(
92 query = query.filter(
93 or_(
93 or_(
94 User.name.ilike(ilike_expression),
94 User.name.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
96 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
97 )
97 )
98 )
98 )
99 # sort by len to have top most matches first
99 # sort by len to have top most matches first
100 query = query.order_by(func.length(User.username))\
100 query = query.order_by(func.length(User.username))\
101 .order_by(User.username)
101 .order_by(User.username)
102 query = query.limit(limit)
102 query = query.limit(limit)
103
103
104 users = query.all()
104 users = query.all()
105
105
106 _users = [
106 _users = [
107 self._serialize_user(user) for user in users
107 self._serialize_user(user) for user in users
108 ]
108 ]
109 return _users
109 return _users
110
110
111 def get_by_username(self, username, cache=False, case_insensitive=False):
111 def get_by_username(self, username, cache=False, case_insensitive=False):
112
112
113 if case_insensitive:
113 if case_insensitive:
114 user = self.sa.query(User).filter(User.username.ilike(username))
114 user = self.sa.query(User).filter(User.username.ilike(username))
115 else:
115 else:
116 user = self.sa.query(User)\
116 user = self.sa.query(User)\
117 .filter(User.username == username)
117 .filter(User.username == username)
118 if cache:
118 if cache:
119 name_key = _hash_key(username)
119 name_key = _hash_key(username)
120 user = user.options(
120 user = user.options(
121 FromCache("sql_cache_short", "get_user_%s" % name_key))
121 FromCache("sql_cache_short", "get_user_%s" % name_key))
122 return user.scalar()
122 return user.scalar()
123
123
124 def get_by_email(self, email, cache=False, case_insensitive=False):
124 def get_by_email(self, email, cache=False, case_insensitive=False):
125 return User.get_by_email(email, case_insensitive, cache)
125 return User.get_by_email(email, case_insensitive, cache)
126
126
127 def get_by_auth_token(self, auth_token, cache=False):
127 def get_by_auth_token(self, auth_token, cache=False):
128 return User.get_by_auth_token(auth_token, cache)
128 return User.get_by_auth_token(auth_token, cache)
129
129
130 def get_active_user_count(self, cache=False):
130 def get_active_user_count(self, cache=False):
131 qry = User.query().filter(
131 qry = User.query().filter(
132 User.active == true()).filter(
132 User.active == true()).filter(
133 User.username != User.DEFAULT_USER)
133 User.username != User.DEFAULT_USER)
134 if cache:
134 if cache:
135 qry = qry.options(
135 qry = qry.options(
136 FromCache("sql_cache_short", "get_active_users"))
136 FromCache("sql_cache_short", "get_active_users"))
137 return qry.count()
137 return qry.count()
138
138
139 def create(self, form_data, cur_user=None):
139 def create(self, form_data, cur_user=None):
140 if not cur_user:
140 if not cur_user:
141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
142
142
143 user_data = {
143 user_data = {
144 'username': form_data['username'],
144 'username': form_data['username'],
145 'password': form_data['password'],
145 'password': form_data['password'],
146 'email': form_data['email'],
146 'email': form_data['email'],
147 'firstname': form_data['firstname'],
147 'firstname': form_data['firstname'],
148 'lastname': form_data['lastname'],
148 'lastname': form_data['lastname'],
149 'active': form_data['active'],
149 'active': form_data['active'],
150 'extern_type': form_data['extern_type'],
150 'extern_type': form_data['extern_type'],
151 'extern_name': form_data['extern_name'],
151 'extern_name': form_data['extern_name'],
152 'admin': False,
152 'admin': False,
153 'cur_user': cur_user
153 'cur_user': cur_user
154 }
154 }
155
155
156 if 'create_repo_group' in form_data:
156 if 'create_repo_group' in form_data:
157 user_data['create_repo_group'] = str2bool(
157 user_data['create_repo_group'] = str2bool(
158 form_data.get('create_repo_group'))
158 form_data.get('create_repo_group'))
159
159
160 try:
160 try:
161 if form_data.get('password_change'):
161 if form_data.get('password_change'):
162 user_data['force_password_change'] = True
162 user_data['force_password_change'] = True
163 return UserModel().create_or_update(**user_data)
163 return UserModel().create_or_update(**user_data)
164 except Exception:
164 except Exception:
165 log.error(traceback.format_exc())
165 log.error(traceback.format_exc())
166 raise
166 raise
167
167
168 def update_user(self, user, skip_attrs=None, **kwargs):
168 def update_user(self, user, skip_attrs=None, **kwargs):
169 from rhodecode.lib.auth import get_crypt_password
169 from rhodecode.lib.auth import get_crypt_password
170
170
171 user = self._get_user(user)
171 user = self._get_user(user)
172 if user.username == User.DEFAULT_USER:
172 if user.username == User.DEFAULT_USER:
173 raise DefaultUserException(
173 raise DefaultUserException(
174 "You can't edit this user (`%(username)s`) since it's "
174 "You can't edit this user (`%(username)s`) since it's "
175 "crucial for entire application" % {
175 "crucial for entire application" % {
176 'username': user.username})
176 'username': user.username})
177
177
178 # first store only defaults
178 # first store only defaults
179 user_attrs = {
179 user_attrs = {
180 'updating_user_id': user.user_id,
180 'updating_user_id': user.user_id,
181 'username': user.username,
181 'username': user.username,
182 'password': user.password,
182 'password': user.password,
183 'email': user.email,
183 'email': user.email,
184 'firstname': user.name,
184 'firstname': user.name,
185 'lastname': user.lastname,
185 'lastname': user.lastname,
186 'description': user.description,
186 'description': user.description,
187 'active': user.active,
187 'active': user.active,
188 'admin': user.admin,
188 'admin': user.admin,
189 'extern_name': user.extern_name,
189 'extern_name': user.extern_name,
190 'extern_type': user.extern_type,
190 'extern_type': user.extern_type,
191 'language': user.user_data.get('language')
191 'language': user.user_data.get('language')
192 }
192 }
193
193
194 # in case there's new_password, that comes from form, use it to
194 # in case there's new_password, that comes from form, use it to
195 # store password
195 # store password
196 if kwargs.get('new_password'):
196 if kwargs.get('new_password'):
197 kwargs['password'] = kwargs['new_password']
197 kwargs['password'] = kwargs['new_password']
198
198
199 # cleanups, my_account password change form
199 # cleanups, my_account password change form
200 kwargs.pop('current_password', None)
200 kwargs.pop('current_password', None)
201 kwargs.pop('new_password', None)
201 kwargs.pop('new_password', None)
202
202
203 # cleanups, user edit password change form
203 # cleanups, user edit password change form
204 kwargs.pop('password_confirmation', None)
204 kwargs.pop('password_confirmation', None)
205 kwargs.pop('password_change', None)
205 kwargs.pop('password_change', None)
206
206
207 # create repo group on user creation
207 # create repo group on user creation
208 kwargs.pop('create_repo_group', None)
208 kwargs.pop('create_repo_group', None)
209
209
210 # legacy forms send name, which is the firstname
210 # legacy forms send name, which is the firstname
211 firstname = kwargs.pop('name', None)
211 firstname = kwargs.pop('name', None)
212 if firstname:
212 if firstname:
213 kwargs['firstname'] = firstname
213 kwargs['firstname'] = firstname
214
214
215 for k, v in kwargs.items():
215 for k, v in kwargs.items():
216 # skip if we don't want to update this
216 # skip if we don't want to update this
217 if skip_attrs and k in skip_attrs:
217 if skip_attrs and k in skip_attrs:
218 continue
218 continue
219
219
220 user_attrs[k] = v
220 user_attrs[k] = v
221
221
222 try:
222 try:
223 return self.create_or_update(**user_attrs)
223 return self.create_or_update(**user_attrs)
224 except Exception:
224 except Exception:
225 log.error(traceback.format_exc())
225 log.error(traceback.format_exc())
226 raise
226 raise
227
227
228 def create_or_update(
228 def create_or_update(
229 self, username, password, email, firstname='', lastname='',
229 self, username, password, email, firstname='', lastname='',
230 active=True, admin=False, extern_type=None, extern_name=None,
230 active=True, admin=False, extern_type=None, extern_name=None,
231 cur_user=None, plugin=None, force_password_change=False,
231 cur_user=None, plugin=None, force_password_change=False,
232 allow_to_create_user=True, create_repo_group=None,
232 allow_to_create_user=True, create_repo_group=None,
233 updating_user_id=None, language=None, description='',
233 updating_user_id=None, language=None, description='',
234 strict_creation_check=True):
234 strict_creation_check=True):
235 """
235 """
236 Creates a new instance if not found, or updates current one
236 Creates a new instance if not found, or updates current one
237
237
238 :param username:
238 :param username:
239 :param password:
239 :param password:
240 :param email:
240 :param email:
241 :param firstname:
241 :param firstname:
242 :param lastname:
242 :param lastname:
243 :param active:
243 :param active:
244 :param admin:
244 :param admin:
245 :param extern_type:
245 :param extern_type:
246 :param extern_name:
246 :param extern_name:
247 :param cur_user:
247 :param cur_user:
248 :param plugin: optional plugin this method was called from
248 :param plugin: optional plugin this method was called from
249 :param force_password_change: toggles new or existing user flag
249 :param force_password_change: toggles new or existing user flag
250 for password change
250 for password change
251 :param allow_to_create_user: Defines if the method can actually create
251 :param allow_to_create_user: Defines if the method can actually create
252 new users
252 new users
253 :param create_repo_group: Defines if the method should also
253 :param create_repo_group: Defines if the method should also
254 create an repo group with user name, and owner
254 create an repo group with user name, and owner
255 :param updating_user_id: if we set it up this is the user we want to
255 :param updating_user_id: if we set it up this is the user we want to
256 update this allows to editing username.
256 update this allows to editing username.
257 :param language: language of user from interface.
257 :param language: language of user from interface.
258 :param description: user description
258 :param description: user description
259 :param strict_creation_check: checks for allowed creation license wise etc.
259 :param strict_creation_check: checks for allowed creation license wise etc.
260
260
261 :returns: new User object with injected `is_new_user` attribute.
261 :returns: new User object with injected `is_new_user` attribute.
262 """
262 """
263
263
264 if not cur_user:
264 if not cur_user:
265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
266
266
267 from rhodecode.lib.auth import (
267 from rhodecode.lib.auth import (
268 get_crypt_password, check_password)
268 get_crypt_password, check_password)
269 from rhodecode.lib import hooks_base
269 from rhodecode.lib import hooks_base
270
270
271 def _password_change(new_user, password):
271 def _password_change(new_user, password):
272 old_password = new_user.password or ''
272 old_password = new_user.password or ''
273 # empty password
273 # empty password
274 if not old_password:
274 if not old_password:
275 return False
275 return False
276
276
277 # password check is only needed for RhodeCode internal auth calls
277 # password check is only needed for RhodeCode internal auth calls
278 # in case it's a plugin we don't care
278 # in case it's a plugin we don't care
279 if not plugin:
279 if not plugin:
280
280
281 # first check if we gave crypted password back, and if it
281 # first check if we gave crypted password back, and if it
282 # matches it's not password change
282 # matches it's not password change
283 if new_user.password == password:
283 if new_user.password == password:
284 return False
284 return False
285
285
286 password_match = check_password(password, old_password)
286 password_match = check_password(password, old_password)
287 if not password_match:
287 if not password_match:
288 return True
288 return True
289
289
290 return False
290 return False
291
291
292 # read settings on default personal repo group creation
292 # read settings on default personal repo group creation
293 if create_repo_group is None:
293 if create_repo_group is None:
294 default_create_repo_group = RepoGroupModel()\
294 default_create_repo_group = RepoGroupModel()\
295 .get_default_create_personal_repo_group()
295 .get_default_create_personal_repo_group()
296 create_repo_group = default_create_repo_group
296 create_repo_group = default_create_repo_group
297
297
298 user_data = {
298 user_data = {
299 'username': username,
299 'username': username,
300 'password': password,
300 'password': password,
301 'email': email,
301 'email': email,
302 'firstname': firstname,
302 'firstname': firstname,
303 'lastname': lastname,
303 'lastname': lastname,
304 'active': active,
304 'active': active,
305 'admin': admin
305 'admin': admin
306 }
306 }
307
307
308 if updating_user_id:
308 if updating_user_id:
309 log.debug('Checking for existing account in RhodeCode '
309 log.debug('Checking for existing account in RhodeCode '
310 'database with user_id `%s` ', updating_user_id)
310 'database with user_id `%s` ', updating_user_id)
311 user = User.get(updating_user_id)
311 user = User.get(updating_user_id)
312 else:
312 else:
313 log.debug('Checking for existing account in RhodeCode '
313 log.debug('Checking for existing account in RhodeCode '
314 'database with username `%s` ', username)
314 'database with username `%s` ', username)
315 user = User.get_by_username(username, case_insensitive=True)
315 user = User.get_by_username(username, case_insensitive=True)
316
316
317 if user is None:
317 if user is None:
318 # we check internal flag if this method is actually allowed to
318 # we check internal flag if this method is actually allowed to
319 # create new user
319 # create new user
320 if not allow_to_create_user:
320 if not allow_to_create_user:
321 msg = ('Method wants to create new user, but it is not '
321 msg = ('Method wants to create new user, but it is not '
322 'allowed to do so')
322 'allowed to do so')
323 log.warning(msg)
323 log.warning(msg)
324 raise NotAllowedToCreateUserError(msg)
324 raise NotAllowedToCreateUserError(msg)
325
325
326 log.debug('Creating new user %s', username)
326 log.debug('Creating new user %s', username)
327
327
328 # only if we create user that is active
328 # only if we create user that is active
329 new_active_user = active
329 new_active_user = active
330 if new_active_user and strict_creation_check:
330 if new_active_user and strict_creation_check:
331 # raises UserCreationError if it's not allowed for any reason to
331 # raises UserCreationError if it's not allowed for any reason to
332 # create new active user, this also executes pre-create hooks
332 # create new active user, this also executes pre-create hooks
333 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
333 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
334 events.trigger(events.UserPreCreate(user_data))
334 events.trigger(events.UserPreCreate(user_data))
335 new_user = User()
335 new_user = User()
336 edit = False
336 edit = False
337 else:
337 else:
338 log.debug('updating user `%s`', username)
338 log.debug('updating user `%s`', username)
339 events.trigger(events.UserPreUpdate(user, user_data))
339 events.trigger(events.UserPreUpdate(user, user_data))
340 new_user = user
340 new_user = user
341 edit = True
341 edit = True
342
342
343 # we're not allowed to edit default user
343 # we're not allowed to edit default user
344 if user.username == User.DEFAULT_USER:
344 if user.username == User.DEFAULT_USER:
345 raise DefaultUserException(
345 raise DefaultUserException(
346 "You can't edit this user (`%(username)s`) since it's "
346 "You can't edit this user (`%(username)s`) since it's "
347 "crucial for entire application"
347 "crucial for entire application"
348 % {'username': user.username})
348 % {'username': user.username})
349
349
350 # inject special attribute that will tell us if User is new or old
350 # inject special attribute that will tell us if User is new or old
351 new_user.is_new_user = not edit
351 new_user.is_new_user = not edit
352 # for users that didn's specify auth type, we use RhodeCode built in
352 # for users that didn's specify auth type, we use RhodeCode built in
353 from rhodecode.authentication.plugins import auth_rhodecode
353 from rhodecode.authentication.plugins import auth_rhodecode
354 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
354 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
355 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
355 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
356
356
357 try:
357 try:
358 new_user.username = username
358 new_user.username = username
359 new_user.admin = admin
359 new_user.admin = admin
360 new_user.email = email
360 new_user.email = email
361 new_user.active = active
361 new_user.active = active
362 new_user.extern_name = safe_unicode(extern_name)
362 new_user.extern_name = safe_unicode(extern_name)
363 new_user.extern_type = safe_unicode(extern_type)
363 new_user.extern_type = safe_unicode(extern_type)
364 new_user.name = firstname
364 new_user.name = firstname
365 new_user.lastname = lastname
365 new_user.lastname = lastname
366 new_user.description = description
366 new_user.description = description
367
367
368 # set password only if creating an user or password is changed
368 # set password only if creating an user or password is changed
369 if not edit or _password_change(new_user, password):
369 if not edit or _password_change(new_user, password):
370 reason = 'new password' if edit else 'new user'
370 reason = 'new password' if edit else 'new user'
371 log.debug('Updating password reason=>%s', reason)
371 log.debug('Updating password reason=>%s', reason)
372 new_user.password = get_crypt_password(password) if password else None
372 new_user.password = get_crypt_password(password) if password else None
373
373
374 if force_password_change:
374 if force_password_change:
375 new_user.update_userdata(force_password_change=True)
375 new_user.update_userdata(force_password_change=True)
376 if language:
376 if language:
377 new_user.update_userdata(language=language)
377 new_user.update_userdata(language=language)
378 new_user.update_userdata(notification_status=True)
378 new_user.update_userdata(notification_status=True)
379
379
380 self.sa.add(new_user)
380 self.sa.add(new_user)
381
381
382 if not edit and create_repo_group:
382 if not edit and create_repo_group:
383 RepoGroupModel().create_personal_repo_group(
383 RepoGroupModel().create_personal_repo_group(
384 new_user, commit_early=False)
384 new_user, commit_early=False)
385
385
386 if not edit:
386 if not edit:
387 # add the RSS token
387 # add the RSS token
388 self.add_auth_token(
388 self.add_auth_token(
389 user=username, lifetime_minutes=-1,
389 user=username, lifetime_minutes=-1,
390 role=self.auth_token_role.ROLE_FEED,
390 role=self.auth_token_role.ROLE_FEED,
391 description=u'Generated feed token')
391 description=u'Generated feed token')
392
392
393 kwargs = new_user.get_dict()
393 kwargs = new_user.get_dict()
394 # backward compat, require api_keys present
394 # backward compat, require api_keys present
395 kwargs['api_keys'] = kwargs['auth_tokens']
395 kwargs['api_keys'] = kwargs['auth_tokens']
396 hooks_base.create_user(created_by=cur_user, **kwargs)
396 hooks_base.create_user(created_by=cur_user, **kwargs)
397 events.trigger(events.UserPostCreate(user_data))
397 events.trigger(events.UserPostCreate(user_data))
398 return new_user
398 return new_user
399 except (DatabaseError,):
399 except (DatabaseError,):
400 log.error(traceback.format_exc())
400 log.error(traceback.format_exc())
401 raise
401 raise
402
402
403 def create_registration(self, form_data,
403 def create_registration(self, form_data,
404 extern_name='rhodecode', extern_type='rhodecode'):
404 extern_name='rhodecode', extern_type='rhodecode'):
405 from rhodecode.model.notification import NotificationModel
405 from rhodecode.model.notification import NotificationModel
406 from rhodecode.model.notification import EmailNotificationModel
406 from rhodecode.model.notification import EmailNotificationModel
407
407
408 try:
408 try:
409 form_data['admin'] = False
409 form_data['admin'] = False
410 form_data['extern_name'] = extern_name
410 form_data['extern_name'] = extern_name
411 form_data['extern_type'] = extern_type
411 form_data['extern_type'] = extern_type
412 new_user = self.create(form_data)
412 new_user = self.create(form_data)
413
413
414 self.sa.add(new_user)
414 self.sa.add(new_user)
415 self.sa.flush()
415 self.sa.flush()
416
416
417 user_data = new_user.get_dict()
417 user_data = new_user.get_dict()
418 user_data.update({
418 user_data.update({
419 'first_name': user_data.get('firstname'),
419 'first_name': user_data.get('firstname'),
420 'last_name': user_data.get('lastname'),
420 'last_name': user_data.get('lastname'),
421 })
421 })
422 kwargs = {
422 kwargs = {
423 # use SQLALCHEMY safe dump of user data
423 # use SQLALCHEMY safe dump of user data
424 'user': AttributeDict(user_data),
424 'user': AttributeDict(user_data),
425 'date': datetime.datetime.now()
425 'date': datetime.datetime.now()
426 }
426 }
427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
428 # pre-generate the subject for notification itself
429 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
430 notification_type, **kwargs)
431
428
432 # create notification objects, and emails
429 # create notification objects, and emails
433 NotificationModel().create(
430 NotificationModel().create(
434 created_by=new_user,
431 created_by=new_user,
435 notification_subject=subject,
432 notification_subject='', # Filled in based on the notification_type
436 notification_body=body_plaintext,
433 notification_body='', # Filled in based on the notification_type
437 notification_type=notification_type,
434 notification_type=notification_type,
438 recipients=None, # all admins
435 recipients=None, # all admins
439 email_kwargs=kwargs,
436 email_kwargs=kwargs,
440 )
437 )
441
438
442 return new_user
439 return new_user
443 except Exception:
440 except Exception:
444 log.error(traceback.format_exc())
441 log.error(traceback.format_exc())
445 raise
442 raise
446
443
447 def _handle_user_repos(self, username, repositories, handle_user,
444 def _handle_user_repos(self, username, repositories, handle_user,
448 handle_mode=None):
445 handle_mode=None):
449
446
450 left_overs = True
447 left_overs = True
451
448
452 from rhodecode.model.repo import RepoModel
449 from rhodecode.model.repo import RepoModel
453
450
454 if handle_mode == 'detach':
451 if handle_mode == 'detach':
455 for obj in repositories:
452 for obj in repositories:
456 obj.user = handle_user
453 obj.user = handle_user
457 # set description we know why we super admin now owns
454 # set description we know why we super admin now owns
458 # additional repositories that were orphaned !
455 # additional repositories that were orphaned !
459 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
456 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
460 self.sa.add(obj)
457 self.sa.add(obj)
461 left_overs = False
458 left_overs = False
462 elif handle_mode == 'delete':
459 elif handle_mode == 'delete':
463 for obj in repositories:
460 for obj in repositories:
464 RepoModel().delete(obj, forks='detach')
461 RepoModel().delete(obj, forks='detach')
465 left_overs = False
462 left_overs = False
466
463
467 # if nothing is done we have left overs left
464 # if nothing is done we have left overs left
468 return left_overs
465 return left_overs
469
466
470 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
467 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
471 handle_mode=None):
468 handle_mode=None):
472
469
473 left_overs = True
470 left_overs = True
474
471
475 from rhodecode.model.repo_group import RepoGroupModel
472 from rhodecode.model.repo_group import RepoGroupModel
476
473
477 if handle_mode == 'detach':
474 if handle_mode == 'detach':
478 for r in repository_groups:
475 for r in repository_groups:
479 r.user = handle_user
476 r.user = handle_user
480 # set description we know why we super admin now owns
477 # set description we know why we super admin now owns
481 # additional repositories that were orphaned !
478 # additional repositories that were orphaned !
482 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
479 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
483 r.personal = False
480 r.personal = False
484 self.sa.add(r)
481 self.sa.add(r)
485 left_overs = False
482 left_overs = False
486 elif handle_mode == 'delete':
483 elif handle_mode == 'delete':
487 for r in repository_groups:
484 for r in repository_groups:
488 RepoGroupModel().delete(r)
485 RepoGroupModel().delete(r)
489 left_overs = False
486 left_overs = False
490
487
491 # if nothing is done we have left overs left
488 # if nothing is done we have left overs left
492 return left_overs
489 return left_overs
493
490
494 def _handle_user_user_groups(self, username, user_groups, handle_user,
491 def _handle_user_user_groups(self, username, user_groups, handle_user,
495 handle_mode=None):
492 handle_mode=None):
496
493
497 left_overs = True
494 left_overs = True
498
495
499 from rhodecode.model.user_group import UserGroupModel
496 from rhodecode.model.user_group import UserGroupModel
500
497
501 if handle_mode == 'detach':
498 if handle_mode == 'detach':
502 for r in user_groups:
499 for r in user_groups:
503 for user_user_group_to_perm in r.user_user_group_to_perm:
500 for user_user_group_to_perm in r.user_user_group_to_perm:
504 if user_user_group_to_perm.user.username == username:
501 if user_user_group_to_perm.user.username == username:
505 user_user_group_to_perm.user = handle_user
502 user_user_group_to_perm.user = handle_user
506 r.user = handle_user
503 r.user = handle_user
507 # set description we know why we super admin now owns
504 # set description we know why we super admin now owns
508 # additional repositories that were orphaned !
505 # additional repositories that were orphaned !
509 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
506 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
510 self.sa.add(r)
507 self.sa.add(r)
511 left_overs = False
508 left_overs = False
512 elif handle_mode == 'delete':
509 elif handle_mode == 'delete':
513 for r in user_groups:
510 for r in user_groups:
514 UserGroupModel().delete(r)
511 UserGroupModel().delete(r)
515 left_overs = False
512 left_overs = False
516
513
517 # if nothing is done we have left overs left
514 # if nothing is done we have left overs left
518 return left_overs
515 return left_overs
519
516
520 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
517 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
521 handle_mode=None):
518 handle_mode=None):
522 left_overs = True
519 left_overs = True
523
520
524 from rhodecode.model.pull_request import PullRequestModel
521 from rhodecode.model.pull_request import PullRequestModel
525
522
526 if handle_mode == 'detach':
523 if handle_mode == 'detach':
527 for pr in pull_requests:
524 for pr in pull_requests:
528 pr.user_id = handle_user.user_id
525 pr.user_id = handle_user.user_id
529 # set description we know why we super admin now owns
526 # set description we know why we super admin now owns
530 # additional repositories that were orphaned !
527 # additional repositories that were orphaned !
531 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
528 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
532 self.sa.add(pr)
529 self.sa.add(pr)
533 left_overs = False
530 left_overs = False
534 elif handle_mode == 'delete':
531 elif handle_mode == 'delete':
535 for pr in pull_requests:
532 for pr in pull_requests:
536 PullRequestModel().delete(pr)
533 PullRequestModel().delete(pr)
537
534
538 left_overs = False
535 left_overs = False
539
536
540 # if nothing is done we have left overs left
537 # if nothing is done we have left overs left
541 return left_overs
538 return left_overs
542
539
543 def _handle_user_artifacts(self, username, artifacts, handle_user,
540 def _handle_user_artifacts(self, username, artifacts, handle_user,
544 handle_mode=None):
541 handle_mode=None):
545
542
546 left_overs = True
543 left_overs = True
547
544
548 if handle_mode == 'detach':
545 if handle_mode == 'detach':
549 for a in artifacts:
546 for a in artifacts:
550 a.upload_user = handle_user
547 a.upload_user = handle_user
551 # set description we know why we super admin now owns
548 # set description we know why we super admin now owns
552 # additional artifacts that were orphaned !
549 # additional artifacts that were orphaned !
553 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
550 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
554 self.sa.add(a)
551 self.sa.add(a)
555 left_overs = False
552 left_overs = False
556 elif handle_mode == 'delete':
553 elif handle_mode == 'delete':
557 from rhodecode.apps.file_store import utils as store_utils
554 from rhodecode.apps.file_store import utils as store_utils
558 request = get_current_request()
555 request = get_current_request()
559 storage = store_utils.get_file_storage(request.registry.settings)
556 storage = store_utils.get_file_storage(request.registry.settings)
560 for a in artifacts:
557 for a in artifacts:
561 file_uid = a.file_uid
558 file_uid = a.file_uid
562 storage.delete(file_uid)
559 storage.delete(file_uid)
563 self.sa.delete(a)
560 self.sa.delete(a)
564
561
565 left_overs = False
562 left_overs = False
566
563
567 # if nothing is done we have left overs left
564 # if nothing is done we have left overs left
568 return left_overs
565 return left_overs
569
566
570 def delete(self, user, cur_user=None, handle_repos=None,
567 def delete(self, user, cur_user=None, handle_repos=None,
571 handle_repo_groups=None, handle_user_groups=None,
568 handle_repo_groups=None, handle_user_groups=None,
572 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
569 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
573 from rhodecode.lib import hooks_base
570 from rhodecode.lib import hooks_base
574
571
575 if not cur_user:
572 if not cur_user:
576 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
573 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
577
574
578 user = self._get_user(user)
575 user = self._get_user(user)
579
576
580 try:
577 try:
581 if user.username == User.DEFAULT_USER:
578 if user.username == User.DEFAULT_USER:
582 raise DefaultUserException(
579 raise DefaultUserException(
583 u"You can't remove this user since it's"
580 u"You can't remove this user since it's"
584 u" crucial for entire application")
581 u" crucial for entire application")
585 handle_user = handle_new_owner or self.cls.get_first_super_admin()
582 handle_user = handle_new_owner or self.cls.get_first_super_admin()
586 log.debug('New detached objects owner %s', handle_user)
583 log.debug('New detached objects owner %s', handle_user)
587
584
588 left_overs = self._handle_user_repos(
585 left_overs = self._handle_user_repos(
589 user.username, user.repositories, handle_user, handle_repos)
586 user.username, user.repositories, handle_user, handle_repos)
590 if left_overs and user.repositories:
587 if left_overs and user.repositories:
591 repos = [x.repo_name for x in user.repositories]
588 repos = [x.repo_name for x in user.repositories]
592 raise UserOwnsReposException(
589 raise UserOwnsReposException(
593 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
590 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
594 u'removed. Switch owners or remove those repositories:%(list_repos)s'
591 u'removed. Switch owners or remove those repositories:%(list_repos)s'
595 % {'username': user.username, 'len_repos': len(repos),
592 % {'username': user.username, 'len_repos': len(repos),
596 'list_repos': ', '.join(repos)})
593 'list_repos': ', '.join(repos)})
597
594
598 left_overs = self._handle_user_repo_groups(
595 left_overs = self._handle_user_repo_groups(
599 user.username, user.repository_groups, handle_user, handle_repo_groups)
596 user.username, user.repository_groups, handle_user, handle_repo_groups)
600 if left_overs and user.repository_groups:
597 if left_overs and user.repository_groups:
601 repo_groups = [x.group_name for x in user.repository_groups]
598 repo_groups = [x.group_name for x in user.repository_groups]
602 raise UserOwnsRepoGroupsException(
599 raise UserOwnsRepoGroupsException(
603 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
600 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
604 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
601 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
605 % {'username': user.username, 'len_repo_groups': len(repo_groups),
602 % {'username': user.username, 'len_repo_groups': len(repo_groups),
606 'list_repo_groups': ', '.join(repo_groups)})
603 'list_repo_groups': ', '.join(repo_groups)})
607
604
608 left_overs = self._handle_user_user_groups(
605 left_overs = self._handle_user_user_groups(
609 user.username, user.user_groups, handle_user, handle_user_groups)
606 user.username, user.user_groups, handle_user, handle_user_groups)
610 if left_overs and user.user_groups:
607 if left_overs and user.user_groups:
611 user_groups = [x.users_group_name for x in user.user_groups]
608 user_groups = [x.users_group_name for x in user.user_groups]
612 raise UserOwnsUserGroupsException(
609 raise UserOwnsUserGroupsException(
613 u'user "%s" still owns %s user groups and cannot be '
610 u'user "%s" still owns %s user groups and cannot be '
614 u'removed. Switch owners or remove those user groups:%s'
611 u'removed. Switch owners or remove those user groups:%s'
615 % (user.username, len(user_groups), ', '.join(user_groups)))
612 % (user.username, len(user_groups), ', '.join(user_groups)))
616
613
617 left_overs = self._handle_user_pull_requests(
614 left_overs = self._handle_user_pull_requests(
618 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
615 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
619 if left_overs and user.user_pull_requests:
616 if left_overs and user.user_pull_requests:
620 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
617 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
621 raise UserOwnsPullRequestsException(
618 raise UserOwnsPullRequestsException(
622 u'user "%s" still owns %s pull requests and cannot be '
619 u'user "%s" still owns %s pull requests and cannot be '
623 u'removed. Switch owners or remove those pull requests:%s'
620 u'removed. Switch owners or remove those pull requests:%s'
624 % (user.username, len(pull_requests), ', '.join(pull_requests)))
621 % (user.username, len(pull_requests), ', '.join(pull_requests)))
625
622
626 left_overs = self._handle_user_artifacts(
623 left_overs = self._handle_user_artifacts(
627 user.username, user.artifacts, handle_user, handle_artifacts)
624 user.username, user.artifacts, handle_user, handle_artifacts)
628 if left_overs and user.artifacts:
625 if left_overs and user.artifacts:
629 artifacts = [x.file_uid for x in user.artifacts]
626 artifacts = [x.file_uid for x in user.artifacts]
630 raise UserOwnsArtifactsException(
627 raise UserOwnsArtifactsException(
631 u'user "%s" still owns %s artifacts and cannot be '
628 u'user "%s" still owns %s artifacts and cannot be '
632 u'removed. Switch owners or remove those artifacts:%s'
629 u'removed. Switch owners or remove those artifacts:%s'
633 % (user.username, len(artifacts), ', '.join(artifacts)))
630 % (user.username, len(artifacts), ', '.join(artifacts)))
634
631
635 user_data = user.get_dict() # fetch user data before expire
632 user_data = user.get_dict() # fetch user data before expire
636
633
637 # we might change the user data with detach/delete, make sure
634 # we might change the user data with detach/delete, make sure
638 # the object is marked as expired before actually deleting !
635 # the object is marked as expired before actually deleting !
639 self.sa.expire(user)
636 self.sa.expire(user)
640 self.sa.delete(user)
637 self.sa.delete(user)
641
638
642 hooks_base.delete_user(deleted_by=cur_user, **user_data)
639 hooks_base.delete_user(deleted_by=cur_user, **user_data)
643 except Exception:
640 except Exception:
644 log.error(traceback.format_exc())
641 log.error(traceback.format_exc())
645 raise
642 raise
646
643
647 def reset_password_link(self, data, pwd_reset_url):
644 def reset_password_link(self, data, pwd_reset_url):
648 from rhodecode.lib.celerylib import tasks, run_task
645 from rhodecode.lib.celerylib import tasks, run_task
649 from rhodecode.model.notification import EmailNotificationModel
646 from rhodecode.model.notification import EmailNotificationModel
650 user_email = data['email']
647 user_email = data['email']
651 try:
648 try:
652 user = User.get_by_email(user_email)
649 user = User.get_by_email(user_email)
653 if user:
650 if user:
654 log.debug('password reset user found %s', user)
651 log.debug('password reset user found %s', user)
655
652
656 email_kwargs = {
653 email_kwargs = {
657 'password_reset_url': pwd_reset_url,
654 'password_reset_url': pwd_reset_url,
658 'user': user,
655 'user': user,
659 'email': user_email,
656 'email': user_email,
660 'date': datetime.datetime.now(),
657 'date': datetime.datetime.now(),
661 'first_admin_email': User.get_first_super_admin().email
658 'first_admin_email': User.get_first_super_admin().email
662 }
659 }
663
660
664 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
661 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
662 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666
663
667 recipients = [user_email]
664 recipients = [user_email]
668
665
669 action_logger_generic(
666 action_logger_generic(
670 'sending password reset email to user: {}'.format(
667 'sending password reset email to user: {}'.format(
671 user), namespace='security.password_reset')
668 user), namespace='security.password_reset')
672
669
673 run_task(tasks.send_email, recipients, subject,
670 run_task(tasks.send_email, recipients, subject,
674 email_body_plaintext, email_body)
671 email_body_plaintext, email_body)
675
672
676 else:
673 else:
677 log.debug("password reset email %s not found", user_email)
674 log.debug("password reset email %s not found", user_email)
678 except Exception:
675 except Exception:
679 log.error(traceback.format_exc())
676 log.error(traceback.format_exc())
680 return False
677 return False
681
678
682 return True
679 return True
683
680
684 def reset_password(self, data):
681 def reset_password(self, data):
685 from rhodecode.lib.celerylib import tasks, run_task
682 from rhodecode.lib.celerylib import tasks, run_task
686 from rhodecode.model.notification import EmailNotificationModel
683 from rhodecode.model.notification import EmailNotificationModel
687 from rhodecode.lib import auth
684 from rhodecode.lib import auth
688 user_email = data['email']
685 user_email = data['email']
689 pre_db = True
686 pre_db = True
690 try:
687 try:
691 user = User.get_by_email(user_email)
688 user = User.get_by_email(user_email)
692 new_passwd = auth.PasswordGenerator().gen_password(
689 new_passwd = auth.PasswordGenerator().gen_password(
693 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
690 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
694 if user:
691 if user:
695 user.password = auth.get_crypt_password(new_passwd)
692 user.password = auth.get_crypt_password(new_passwd)
696 # also force this user to reset his password !
693 # also force this user to reset his password !
697 user.update_userdata(force_password_change=True)
694 user.update_userdata(force_password_change=True)
698
695
699 Session().add(user)
696 Session().add(user)
700
697
701 # now delete the token in question
698 # now delete the token in question
702 UserApiKeys = AuthTokenModel.cls
699 UserApiKeys = AuthTokenModel.cls
703 UserApiKeys().query().filter(
700 UserApiKeys().query().filter(
704 UserApiKeys.api_key == data['token']).delete()
701 UserApiKeys.api_key == data['token']).delete()
705
702
706 Session().commit()
703 Session().commit()
707 log.info('successfully reset password for `%s`', user_email)
704 log.info('successfully reset password for `%s`', user_email)
708
705
709 if new_passwd is None:
706 if new_passwd is None:
710 raise Exception('unable to generate new password')
707 raise Exception('unable to generate new password')
711
708
712 pre_db = False
709 pre_db = False
713
710
714 email_kwargs = {
711 email_kwargs = {
715 'new_password': new_passwd,
712 'new_password': new_passwd,
716 'user': user,
713 'user': user,
717 'email': user_email,
714 'email': user_email,
718 'date': datetime.datetime.now(),
715 'date': datetime.datetime.now(),
719 'first_admin_email': User.get_first_super_admin().email
716 'first_admin_email': User.get_first_super_admin().email
720 }
717 }
721
718
722 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
719 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
723 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
720 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 **email_kwargs)
721 **email_kwargs)
725
722
726 recipients = [user_email]
723 recipients = [user_email]
727
724
728 action_logger_generic(
725 action_logger_generic(
729 'sent new password to user: {} with email: {}'.format(
726 'sent new password to user: {} with email: {}'.format(
730 user, user_email), namespace='security.password_reset')
727 user, user_email), namespace='security.password_reset')
731
728
732 run_task(tasks.send_email, recipients, subject,
729 run_task(tasks.send_email, recipients, subject,
733 email_body_plaintext, email_body)
730 email_body_plaintext, email_body)
734
731
735 except Exception:
732 except Exception:
736 log.error('Failed to update user password')
733 log.error('Failed to update user password')
737 log.error(traceback.format_exc())
734 log.error(traceback.format_exc())
738 if pre_db:
735 if pre_db:
739 # we rollback only if local db stuff fails. If it goes into
736 # we rollback only if local db stuff fails. If it goes into
740 # run_task, we're pass rollback state this wouldn't work then
737 # run_task, we're pass rollback state this wouldn't work then
741 Session().rollback()
738 Session().rollback()
742
739
743 return True
740 return True
744
741
745 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
742 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
746 """
743 """
747 Fetches auth_user by user_id,or api_key if present.
744 Fetches auth_user by user_id,or api_key if present.
748 Fills auth_user attributes with those taken from database.
745 Fills auth_user attributes with those taken from database.
749 Additionally set's is_authenitated if lookup fails
746 Additionally set's is_authenitated if lookup fails
750 present in database
747 present in database
751
748
752 :param auth_user: instance of user to set attributes
749 :param auth_user: instance of user to set attributes
753 :param user_id: user id to fetch by
750 :param user_id: user id to fetch by
754 :param api_key: api key to fetch by
751 :param api_key: api key to fetch by
755 :param username: username to fetch by
752 :param username: username to fetch by
756 """
753 """
757 def token_obfuscate(token):
754 def token_obfuscate(token):
758 if token:
755 if token:
759 return token[:4] + "****"
756 return token[:4] + "****"
760
757
761 if user_id is None and api_key is None and username is None:
758 if user_id is None and api_key is None and username is None:
762 raise Exception('You need to pass user_id, api_key or username')
759 raise Exception('You need to pass user_id, api_key or username')
763
760
764 log.debug(
761 log.debug(
765 'AuthUser: fill data execution based on: '
762 'AuthUser: fill data execution based on: '
766 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
763 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
767 try:
764 try:
768 dbuser = None
765 dbuser = None
769 if user_id:
766 if user_id:
770 dbuser = self.get(user_id)
767 dbuser = self.get(user_id)
771 elif api_key:
768 elif api_key:
772 dbuser = self.get_by_auth_token(api_key)
769 dbuser = self.get_by_auth_token(api_key)
773 elif username:
770 elif username:
774 dbuser = self.get_by_username(username)
771 dbuser = self.get_by_username(username)
775
772
776 if not dbuser:
773 if not dbuser:
777 log.warning(
774 log.warning(
778 'Unable to lookup user by id:%s api_key:%s username:%s',
775 'Unable to lookup user by id:%s api_key:%s username:%s',
779 user_id, token_obfuscate(api_key), username)
776 user_id, token_obfuscate(api_key), username)
780 return False
777 return False
781 if not dbuser.active:
778 if not dbuser.active:
782 log.debug('User `%s:%s` is inactive, skipping fill data',
779 log.debug('User `%s:%s` is inactive, skipping fill data',
783 username, user_id)
780 username, user_id)
784 return False
781 return False
785
782
786 log.debug('AuthUser: filling found user:%s data', dbuser)
783 log.debug('AuthUser: filling found user:%s data', dbuser)
787
784
788 attrs = {
785 attrs = {
789 'user_id': dbuser.user_id,
786 'user_id': dbuser.user_id,
790 'username': dbuser.username,
787 'username': dbuser.username,
791 'name': dbuser.name,
788 'name': dbuser.name,
792 'first_name': dbuser.first_name,
789 'first_name': dbuser.first_name,
793 'firstname': dbuser.firstname,
790 'firstname': dbuser.firstname,
794 'last_name': dbuser.last_name,
791 'last_name': dbuser.last_name,
795 'lastname': dbuser.lastname,
792 'lastname': dbuser.lastname,
796 'admin': dbuser.admin,
793 'admin': dbuser.admin,
797 'active': dbuser.active,
794 'active': dbuser.active,
798
795
799 'email': dbuser.email,
796 'email': dbuser.email,
800 'emails': dbuser.emails_cached(),
797 'emails': dbuser.emails_cached(),
801 'short_contact': dbuser.short_contact,
798 'short_contact': dbuser.short_contact,
802 'full_contact': dbuser.full_contact,
799 'full_contact': dbuser.full_contact,
803 'full_name': dbuser.full_name,
800 'full_name': dbuser.full_name,
804 'full_name_or_username': dbuser.full_name_or_username,
801 'full_name_or_username': dbuser.full_name_or_username,
805
802
806 '_api_key': dbuser._api_key,
803 '_api_key': dbuser._api_key,
807 '_user_data': dbuser._user_data,
804 '_user_data': dbuser._user_data,
808
805
809 'created_on': dbuser.created_on,
806 'created_on': dbuser.created_on,
810 'extern_name': dbuser.extern_name,
807 'extern_name': dbuser.extern_name,
811 'extern_type': dbuser.extern_type,
808 'extern_type': dbuser.extern_type,
812
809
813 'inherit_default_permissions': dbuser.inherit_default_permissions,
810 'inherit_default_permissions': dbuser.inherit_default_permissions,
814
811
815 'language': dbuser.language,
812 'language': dbuser.language,
816 'last_activity': dbuser.last_activity,
813 'last_activity': dbuser.last_activity,
817 'last_login': dbuser.last_login,
814 'last_login': dbuser.last_login,
818 'password': dbuser.password,
815 'password': dbuser.password,
819 }
816 }
820 auth_user.__dict__.update(attrs)
817 auth_user.__dict__.update(attrs)
821 except Exception:
818 except Exception:
822 log.error(traceback.format_exc())
819 log.error(traceback.format_exc())
823 auth_user.is_authenticated = False
820 auth_user.is_authenticated = False
824 return False
821 return False
825
822
826 return True
823 return True
827
824
828 def has_perm(self, user, perm):
825 def has_perm(self, user, perm):
829 perm = self._get_perm(perm)
826 perm = self._get_perm(perm)
830 user = self._get_user(user)
827 user = self._get_user(user)
831
828
832 return UserToPerm.query().filter(UserToPerm.user == user)\
829 return UserToPerm.query().filter(UserToPerm.user == user)\
833 .filter(UserToPerm.permission == perm).scalar() is not None
830 .filter(UserToPerm.permission == perm).scalar() is not None
834
831
835 def grant_perm(self, user, perm):
832 def grant_perm(self, user, perm):
836 """
833 """
837 Grant user global permissions
834 Grant user global permissions
838
835
839 :param user:
836 :param user:
840 :param perm:
837 :param perm:
841 """
838 """
842 user = self._get_user(user)
839 user = self._get_user(user)
843 perm = self._get_perm(perm)
840 perm = self._get_perm(perm)
844 # if this permission is already granted skip it
841 # if this permission is already granted skip it
845 _perm = UserToPerm.query()\
842 _perm = UserToPerm.query()\
846 .filter(UserToPerm.user == user)\
843 .filter(UserToPerm.user == user)\
847 .filter(UserToPerm.permission == perm)\
844 .filter(UserToPerm.permission == perm)\
848 .scalar()
845 .scalar()
849 if _perm:
846 if _perm:
850 return
847 return
851 new = UserToPerm()
848 new = UserToPerm()
852 new.user = user
849 new.user = user
853 new.permission = perm
850 new.permission = perm
854 self.sa.add(new)
851 self.sa.add(new)
855 return new
852 return new
856
853
857 def revoke_perm(self, user, perm):
854 def revoke_perm(self, user, perm):
858 """
855 """
859 Revoke users global permissions
856 Revoke users global permissions
860
857
861 :param user:
858 :param user:
862 :param perm:
859 :param perm:
863 """
860 """
864 user = self._get_user(user)
861 user = self._get_user(user)
865 perm = self._get_perm(perm)
862 perm = self._get_perm(perm)
866
863
867 obj = UserToPerm.query()\
864 obj = UserToPerm.query()\
868 .filter(UserToPerm.user == user)\
865 .filter(UserToPerm.user == user)\
869 .filter(UserToPerm.permission == perm)\
866 .filter(UserToPerm.permission == perm)\
870 .scalar()
867 .scalar()
871 if obj:
868 if obj:
872 self.sa.delete(obj)
869 self.sa.delete(obj)
873
870
874 def add_extra_email(self, user, email):
871 def add_extra_email(self, user, email):
875 """
872 """
876 Adds email address to UserEmailMap
873 Adds email address to UserEmailMap
877
874
878 :param user:
875 :param user:
879 :param email:
876 :param email:
880 """
877 """
881
878
882 user = self._get_user(user)
879 user = self._get_user(user)
883
880
884 obj = UserEmailMap()
881 obj = UserEmailMap()
885 obj.user = user
882 obj.user = user
886 obj.email = email
883 obj.email = email
887 self.sa.add(obj)
884 self.sa.add(obj)
888 return obj
885 return obj
889
886
890 def delete_extra_email(self, user, email_id):
887 def delete_extra_email(self, user, email_id):
891 """
888 """
892 Removes email address from UserEmailMap
889 Removes email address from UserEmailMap
893
890
894 :param user:
891 :param user:
895 :param email_id:
892 :param email_id:
896 """
893 """
897 user = self._get_user(user)
894 user = self._get_user(user)
898 obj = UserEmailMap.query().get(email_id)
895 obj = UserEmailMap.query().get(email_id)
899 if obj and obj.user_id == user.user_id:
896 if obj and obj.user_id == user.user_id:
900 self.sa.delete(obj)
897 self.sa.delete(obj)
901
898
902 def parse_ip_range(self, ip_range):
899 def parse_ip_range(self, ip_range):
903 ip_list = []
900 ip_list = []
904
901
905 def make_unique(value):
902 def make_unique(value):
906 seen = []
903 seen = []
907 return [c for c in value if not (c in seen or seen.append(c))]
904 return [c for c in value if not (c in seen or seen.append(c))]
908
905
909 # firsts split by commas
906 # firsts split by commas
910 for ip_range in ip_range.split(','):
907 for ip_range in ip_range.split(','):
911 if not ip_range:
908 if not ip_range:
912 continue
909 continue
913 ip_range = ip_range.strip()
910 ip_range = ip_range.strip()
914 if '-' in ip_range:
911 if '-' in ip_range:
915 start_ip, end_ip = ip_range.split('-', 1)
912 start_ip, end_ip = ip_range.split('-', 1)
916 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
913 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
917 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
914 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
918 parsed_ip_range = []
915 parsed_ip_range = []
919
916
920 for index in range(int(start_ip), int(end_ip) + 1):
917 for index in range(int(start_ip), int(end_ip) + 1):
921 new_ip = ipaddress.ip_address(index)
918 new_ip = ipaddress.ip_address(index)
922 parsed_ip_range.append(str(new_ip))
919 parsed_ip_range.append(str(new_ip))
923 ip_list.extend(parsed_ip_range)
920 ip_list.extend(parsed_ip_range)
924 else:
921 else:
925 ip_list.append(ip_range)
922 ip_list.append(ip_range)
926
923
927 return make_unique(ip_list)
924 return make_unique(ip_list)
928
925
929 def add_extra_ip(self, user, ip, description=None):
926 def add_extra_ip(self, user, ip, description=None):
930 """
927 """
931 Adds ip address to UserIpMap
928 Adds ip address to UserIpMap
932
929
933 :param user:
930 :param user:
934 :param ip:
931 :param ip:
935 """
932 """
936
933
937 user = self._get_user(user)
934 user = self._get_user(user)
938 obj = UserIpMap()
935 obj = UserIpMap()
939 obj.user = user
936 obj.user = user
940 obj.ip_addr = ip
937 obj.ip_addr = ip
941 obj.description = description
938 obj.description = description
942 self.sa.add(obj)
939 self.sa.add(obj)
943 return obj
940 return obj
944
941
945 auth_token_role = AuthTokenModel.cls
942 auth_token_role = AuthTokenModel.cls
946
943
947 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
944 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
948 scope_callback=None):
945 scope_callback=None):
949 """
946 """
950 Add AuthToken for user.
947 Add AuthToken for user.
951
948
952 :param user: username/user_id
949 :param user: username/user_id
953 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
950 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
954 :param role: one of AuthTokenModel.cls.ROLE_*
951 :param role: one of AuthTokenModel.cls.ROLE_*
955 :param description: optional string description
952 :param description: optional string description
956 """
953 """
957
954
958 token = AuthTokenModel().create(
955 token = AuthTokenModel().create(
959 user, description, lifetime_minutes, role)
956 user, description, lifetime_minutes, role)
960 if scope_callback and callable(scope_callback):
957 if scope_callback and callable(scope_callback):
961 # call the callback if we provide, used to attach scope for EE edition
958 # call the callback if we provide, used to attach scope for EE edition
962 scope_callback(token)
959 scope_callback(token)
963 return token
960 return token
964
961
965 def delete_extra_ip(self, user, ip_id):
962 def delete_extra_ip(self, user, ip_id):
966 """
963 """
967 Removes ip address from UserIpMap
964 Removes ip address from UserIpMap
968
965
969 :param user:
966 :param user:
970 :param ip_id:
967 :param ip_id:
971 """
968 """
972 user = self._get_user(user)
969 user = self._get_user(user)
973 obj = UserIpMap.query().get(ip_id)
970 obj = UserIpMap.query().get(ip_id)
974 if obj and obj.user_id == user.user_id:
971 if obj and obj.user_id == user.user_id:
975 self.sa.delete(obj)
972 self.sa.delete(obj)
976
973
977 def get_accounts_in_creation_order(self, current_user=None):
974 def get_accounts_in_creation_order(self, current_user=None):
978 """
975 """
979 Get accounts in order of creation for deactivation for license limits
976 Get accounts in order of creation for deactivation for license limits
980
977
981 pick currently logged in user, and append to the list in position 0
978 pick currently logged in user, and append to the list in position 0
982 pick all super-admins in order of creation date and add it to the list
979 pick all super-admins in order of creation date and add it to the list
983 pick all other accounts in order of creation and add it to the list.
980 pick all other accounts in order of creation and add it to the list.
984
981
985 Based on that list, the last accounts can be disabled as they are
982 Based on that list, the last accounts can be disabled as they are
986 created at the end and don't include any of the super admins as well
983 created at the end and don't include any of the super admins as well
987 as the current user.
984 as the current user.
988
985
989 :param current_user: optionally current user running this operation
986 :param current_user: optionally current user running this operation
990 """
987 """
991
988
992 if not current_user:
989 if not current_user:
993 current_user = get_current_rhodecode_user()
990 current_user = get_current_rhodecode_user()
994 active_super_admins = [
991 active_super_admins = [
995 x.user_id for x in User.query()
992 x.user_id for x in User.query()
996 .filter(User.user_id != current_user.user_id)
993 .filter(User.user_id != current_user.user_id)
997 .filter(User.active == true())
994 .filter(User.active == true())
998 .filter(User.admin == true())
995 .filter(User.admin == true())
999 .order_by(User.created_on.asc())]
996 .order_by(User.created_on.asc())]
1000
997
1001 active_regular_users = [
998 active_regular_users = [
1002 x.user_id for x in User.query()
999 x.user_id for x in User.query()
1003 .filter(User.user_id != current_user.user_id)
1000 .filter(User.user_id != current_user.user_id)
1004 .filter(User.active == true())
1001 .filter(User.active == true())
1005 .filter(User.admin == false())
1002 .filter(User.admin == false())
1006 .order_by(User.created_on.asc())]
1003 .order_by(User.created_on.asc())]
1007
1004
1008 list_of_accounts = [current_user.user_id]
1005 list_of_accounts = [current_user.user_id]
1009 list_of_accounts += active_super_admins
1006 list_of_accounts += active_super_admins
1010 list_of_accounts += active_regular_users
1007 list_of_accounts += active_regular_users
1011
1008
1012 return list_of_accounts
1009 return list_of_accounts
1013
1010
1014 def deactivate_last_users(self, expected_users, current_user=None):
1011 def deactivate_last_users(self, expected_users, current_user=None):
1015 """
1012 """
1016 Deactivate accounts that are over the license limits.
1013 Deactivate accounts that are over the license limits.
1017 Algorithm of which accounts to disabled is based on the formula:
1014 Algorithm of which accounts to disabled is based on the formula:
1018
1015
1019 Get current user, then super admins in creation order, then regular
1016 Get current user, then super admins in creation order, then regular
1020 active users in creation order.
1017 active users in creation order.
1021
1018
1022 Using that list we mark all accounts from the end of it as inactive.
1019 Using that list we mark all accounts from the end of it as inactive.
1023 This way we block only latest created accounts.
1020 This way we block only latest created accounts.
1024
1021
1025 :param expected_users: list of users in special order, we deactivate
1022 :param expected_users: list of users in special order, we deactivate
1026 the end N amount of users from that list
1023 the end N amount of users from that list
1027 """
1024 """
1028
1025
1029 list_of_accounts = self.get_accounts_in_creation_order(
1026 list_of_accounts = self.get_accounts_in_creation_order(
1030 current_user=current_user)
1027 current_user=current_user)
1031
1028
1032 for acc_id in list_of_accounts[expected_users + 1:]:
1029 for acc_id in list_of_accounts[expected_users + 1:]:
1033 user = User.get(acc_id)
1030 user = User.get(acc_id)
1034 log.info('Deactivating account %s for license unlock', user)
1031 log.info('Deactivating account %s for license unlock', user)
1035 user.active = False
1032 user.active = False
1036 Session().add(user)
1033 Session().add(user)
1037 Session().commit()
1034 Session().commit()
1038
1035
1039 return
1036 return
1040
1037
1041 def get_user_log(self, user, filter_term):
1038 def get_user_log(self, user, filter_term):
1042 user_log = UserLog.query()\
1039 user_log = UserLog.query()\
1043 .filter(or_(UserLog.user_id == user.user_id,
1040 .filter(or_(UserLog.user_id == user.user_id,
1044 UserLog.username == user.username))\
1041 UserLog.username == user.username))\
1045 .options(joinedload(UserLog.user))\
1042 .options(joinedload(UserLog.user))\
1046 .options(joinedload(UserLog.repository))\
1043 .options(joinedload(UserLog.repository))\
1047 .order_by(UserLog.action_date.desc())
1044 .order_by(UserLog.action_date.desc())
1048
1045
1049 user_log = user_log_filter(user_log, filter_term)
1046 user_log = user_log_filter(user_log, filter_term)
1050 return user_log
1047 return user_log
General Comments 0
You need to be logged in to leave comments. Login now