##// END OF EJS Templates
comments: expose a function to fetch unresolved TODOs for repository
marcink -
r3433:840bd8bd default
parent child Browse files
Show More
@@ -1,662 +1,672 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from pyramid.threadlocal import get_current_registry, get_current_request
29 from pyramid.threadlocal import get_current_registry, get_current_request
30 from sqlalchemy.sql.expression import null
30 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.functions import coalesce
31 from sqlalchemy.sql.functions import coalesce
32
32
33 from rhodecode.lib import helpers as h, diffs, channelstream
33 from rhodecode.lib import helpers as h, diffs, channelstream
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 from rhodecode.model.notification import NotificationModel
39 from rhodecode.model.notification import NotificationModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.model.notification import EmailNotificationModel
42 from rhodecode.model.notification import EmailNotificationModel
43 from rhodecode.model.validation_schema.schemas import comment_schema
43 from rhodecode.model.validation_schema.schemas import comment_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CommentsModel(BaseModel):
49 class CommentsModel(BaseModel):
50
50
51 cls = ChangesetComment
51 cls = ChangesetComment
52
52
53 DIFF_CONTEXT_BEFORE = 3
53 DIFF_CONTEXT_BEFORE = 3
54 DIFF_CONTEXT_AFTER = 3
54 DIFF_CONTEXT_AFTER = 3
55
55
56 def __get_commit_comment(self, changeset_comment):
56 def __get_commit_comment(self, changeset_comment):
57 return self._get_instance(ChangesetComment, changeset_comment)
57 return self._get_instance(ChangesetComment, changeset_comment)
58
58
59 def __get_pull_request(self, pull_request):
59 def __get_pull_request(self, pull_request):
60 return self._get_instance(PullRequest, pull_request)
60 return self._get_instance(PullRequest, pull_request)
61
61
62 def _extract_mentions(self, s):
62 def _extract_mentions(self, s):
63 user_objects = []
63 user_objects = []
64 for username in extract_mentioned_users(s):
64 for username in extract_mentioned_users(s):
65 user_obj = User.get_by_username(username, case_insensitive=True)
65 user_obj = User.get_by_username(username, case_insensitive=True)
66 if user_obj:
66 if user_obj:
67 user_objects.append(user_obj)
67 user_objects.append(user_obj)
68 return user_objects
68 return user_objects
69
69
70 def _get_renderer(self, global_renderer='rst', request=None):
70 def _get_renderer(self, global_renderer='rst', request=None):
71 request = request or get_current_request()
71 request = request or get_current_request()
72
72
73 try:
73 try:
74 global_renderer = request.call_context.visual.default_renderer
74 global_renderer = request.call_context.visual.default_renderer
75 except AttributeError:
75 except AttributeError:
76 log.debug("Renderer not set, falling back "
76 log.debug("Renderer not set, falling back "
77 "to default renderer '%s'", global_renderer)
77 "to default renderer '%s'", global_renderer)
78 except Exception:
78 except Exception:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 return global_renderer
80 return global_renderer
81
81
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 # group by versions, and count until, and display objects
83 # group by versions, and count until, and display objects
84
84
85 comment_groups = collections.defaultdict(list)
85 comment_groups = collections.defaultdict(list)
86 [comment_groups[
86 [comment_groups[
87 _co.pull_request_version_id].append(_co) for _co in comments]
87 _co.pull_request_version_id].append(_co) for _co in comments]
88
88
89 def yield_comments(pos):
89 def yield_comments(pos):
90 for co in comment_groups[pos]:
90 for co in comment_groups[pos]:
91 yield co
91 yield co
92
92
93 comment_versions = collections.defaultdict(
93 comment_versions = collections.defaultdict(
94 lambda: collections.defaultdict(list))
94 lambda: collections.defaultdict(list))
95 prev_prvid = -1
95 prev_prvid = -1
96 # fake last entry with None, to aggregate on "latest" version which
96 # fake last entry with None, to aggregate on "latest" version which
97 # doesn't have an pull_request_version_id
97 # doesn't have an pull_request_version_id
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 prvid = ver.pull_request_version_id
99 prvid = ver.pull_request_version_id
100 if prev_prvid == -1:
100 if prev_prvid == -1:
101 prev_prvid = prvid
101 prev_prvid = prvid
102
102
103 for co in yield_comments(prvid):
103 for co in yield_comments(prvid):
104 comment_versions[prvid]['at'].append(co)
104 comment_versions[prvid]['at'].append(co)
105
105
106 # save until
106 # save until
107 current = comment_versions[prvid]['at']
107 current = comment_versions[prvid]['at']
108 prev_until = comment_versions[prev_prvid]['until']
108 prev_until = comment_versions[prev_prvid]['until']
109 cur_until = prev_until + current
109 cur_until = prev_until + current
110 comment_versions[prvid]['until'].extend(cur_until)
110 comment_versions[prvid]['until'].extend(cur_until)
111
111
112 # save outdated
112 # save outdated
113 if inline:
113 if inline:
114 outdated = [x for x in cur_until
114 outdated = [x for x in cur_until
115 if x.outdated_at_version(show_version)]
115 if x.outdated_at_version(show_version)]
116 else:
116 else:
117 outdated = [x for x in cur_until
117 outdated = [x for x in cur_until
118 if x.older_than_version(show_version)]
118 if x.older_than_version(show_version)]
119 display = [x for x in cur_until if x not in outdated]
119 display = [x for x in cur_until if x not in outdated]
120
120
121 comment_versions[prvid]['outdated'] = outdated
121 comment_versions[prvid]['outdated'] = outdated
122 comment_versions[prvid]['display'] = display
122 comment_versions[prvid]['display'] = display
123
123
124 prev_prvid = prvid
124 prev_prvid = prvid
125
125
126 return comment_versions
126 return comment_versions
127
127
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
128 def get_repository_unresolved_todos(self, repo):
129 todos = Session().query(ChangesetComment) \
130 .filter(ChangesetComment.repo == repo) \
131 .filter(ChangesetComment.resolved_by == None) \
132 .filter(ChangesetComment.comment_type
133 == ChangesetComment.COMMENT_TYPE_TODO)
134 todos = todos.all()
135
136 return todos
137
138 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
129
139
130 todos = Session().query(ChangesetComment) \
140 todos = Session().query(ChangesetComment) \
131 .filter(ChangesetComment.pull_request == pull_request) \
141 .filter(ChangesetComment.pull_request == pull_request) \
132 .filter(ChangesetComment.resolved_by == None) \
142 .filter(ChangesetComment.resolved_by == None) \
133 .filter(ChangesetComment.comment_type
143 .filter(ChangesetComment.comment_type
134 == ChangesetComment.COMMENT_TYPE_TODO)
144 == ChangesetComment.COMMENT_TYPE_TODO)
135
145
136 if not show_outdated:
146 if not show_outdated:
137 todos = todos.filter(
147 todos = todos.filter(
138 coalesce(ChangesetComment.display_state, '') !=
148 coalesce(ChangesetComment.display_state, '') !=
139 ChangesetComment.COMMENT_OUTDATED)
149 ChangesetComment.COMMENT_OUTDATED)
140
150
141 todos = todos.all()
151 todos = todos.all()
142
152
143 return todos
153 return todos
144
154
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
155 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
146
156
147 todos = Session().query(ChangesetComment) \
157 todos = Session().query(ChangesetComment) \
148 .filter(ChangesetComment.revision == commit_id) \
158 .filter(ChangesetComment.revision == commit_id) \
149 .filter(ChangesetComment.resolved_by == None) \
159 .filter(ChangesetComment.resolved_by == None) \
150 .filter(ChangesetComment.comment_type
160 .filter(ChangesetComment.comment_type
151 == ChangesetComment.COMMENT_TYPE_TODO)
161 == ChangesetComment.COMMENT_TYPE_TODO)
152
162
153 if not show_outdated:
163 if not show_outdated:
154 todos = todos.filter(
164 todos = todos.filter(
155 coalesce(ChangesetComment.display_state, '') !=
165 coalesce(ChangesetComment.display_state, '') !=
156 ChangesetComment.COMMENT_OUTDATED)
166 ChangesetComment.COMMENT_OUTDATED)
157
167
158 todos = todos.all()
168 todos = todos.all()
159
169
160 return todos
170 return todos
161
171
162 def _log_audit_action(self, action, action_data, auth_user, comment):
172 def _log_audit_action(self, action, action_data, auth_user, comment):
163 audit_logger.store(
173 audit_logger.store(
164 action=action,
174 action=action,
165 action_data=action_data,
175 action_data=action_data,
166 user=auth_user,
176 user=auth_user,
167 repo=comment.repo)
177 repo=comment.repo)
168
178
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
179 def create(self, text, repo, user, commit_id=None, pull_request=None,
170 f_path=None, line_no=None, status_change=None,
180 f_path=None, line_no=None, status_change=None,
171 status_change_type=None, comment_type=None,
181 status_change_type=None, comment_type=None,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
182 resolves_comment_id=None, closing_pr=False, send_email=True,
173 renderer=None, auth_user=None):
183 renderer=None, auth_user=None):
174 """
184 """
175 Creates new comment for commit or pull request.
185 Creates new comment for commit or pull request.
176 IF status_change is not none this comment is associated with a
186 IF status_change is not none this comment is associated with a
177 status change of commit or commit associated with pull request
187 status change of commit or commit associated with pull request
178
188
179 :param text:
189 :param text:
180 :param repo:
190 :param repo:
181 :param user:
191 :param user:
182 :param commit_id:
192 :param commit_id:
183 :param pull_request:
193 :param pull_request:
184 :param f_path:
194 :param f_path:
185 :param line_no:
195 :param line_no:
186 :param status_change: Label for status change
196 :param status_change: Label for status change
187 :param comment_type: Type of comment
197 :param comment_type: Type of comment
188 :param status_change_type: type of status change
198 :param status_change_type: type of status change
189 :param closing_pr:
199 :param closing_pr:
190 :param send_email:
200 :param send_email:
191 :param renderer: pick renderer for this comment
201 :param renderer: pick renderer for this comment
192 """
202 """
193
203
194 if not text:
204 if not text:
195 log.warning('Missing text for comment, skipping...')
205 log.warning('Missing text for comment, skipping...')
196 return
206 return
197 request = get_current_request()
207 request = get_current_request()
198 _ = request.translate
208 _ = request.translate
199
209
200 if not renderer:
210 if not renderer:
201 renderer = self._get_renderer(request=request)
211 renderer = self._get_renderer(request=request)
202
212
203 repo = self._get_repo(repo)
213 repo = self._get_repo(repo)
204 user = self._get_user(user)
214 user = self._get_user(user)
205 auth_user = auth_user or user
215 auth_user = auth_user or user
206
216
207 schema = comment_schema.CommentSchema()
217 schema = comment_schema.CommentSchema()
208 validated_kwargs = schema.deserialize(dict(
218 validated_kwargs = schema.deserialize(dict(
209 comment_body=text,
219 comment_body=text,
210 comment_type=comment_type,
220 comment_type=comment_type,
211 comment_file=f_path,
221 comment_file=f_path,
212 comment_line=line_no,
222 comment_line=line_no,
213 renderer_type=renderer,
223 renderer_type=renderer,
214 status_change=status_change_type,
224 status_change=status_change_type,
215 resolves_comment_id=resolves_comment_id,
225 resolves_comment_id=resolves_comment_id,
216 repo=repo.repo_id,
226 repo=repo.repo_id,
217 user=user.user_id,
227 user=user.user_id,
218 ))
228 ))
219
229
220 comment = ChangesetComment()
230 comment = ChangesetComment()
221 comment.renderer = validated_kwargs['renderer_type']
231 comment.renderer = validated_kwargs['renderer_type']
222 comment.text = validated_kwargs['comment_body']
232 comment.text = validated_kwargs['comment_body']
223 comment.f_path = validated_kwargs['comment_file']
233 comment.f_path = validated_kwargs['comment_file']
224 comment.line_no = validated_kwargs['comment_line']
234 comment.line_no = validated_kwargs['comment_line']
225 comment.comment_type = validated_kwargs['comment_type']
235 comment.comment_type = validated_kwargs['comment_type']
226
236
227 comment.repo = repo
237 comment.repo = repo
228 comment.author = user
238 comment.author = user
229 resolved_comment = self.__get_commit_comment(
239 resolved_comment = self.__get_commit_comment(
230 validated_kwargs['resolves_comment_id'])
240 validated_kwargs['resolves_comment_id'])
231 # check if the comment actually belongs to this PR
241 # check if the comment actually belongs to this PR
232 if resolved_comment and resolved_comment.pull_request and \
242 if resolved_comment and resolved_comment.pull_request and \
233 resolved_comment.pull_request != pull_request:
243 resolved_comment.pull_request != pull_request:
234 # comment not bound to this pull request, forbid
244 # comment not bound to this pull request, forbid
235 resolved_comment = None
245 resolved_comment = None
236 comment.resolved_comment = resolved_comment
246 comment.resolved_comment = resolved_comment
237
247
238 pull_request_id = pull_request
248 pull_request_id = pull_request
239
249
240 commit_obj = None
250 commit_obj = None
241 pull_request_obj = None
251 pull_request_obj = None
242
252
243 if commit_id:
253 if commit_id:
244 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
254 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
245 # do a lookup, so we don't pass something bad here
255 # do a lookup, so we don't pass something bad here
246 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
256 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
247 comment.revision = commit_obj.raw_id
257 comment.revision = commit_obj.raw_id
248
258
249 elif pull_request_id:
259 elif pull_request_id:
250 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
260 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
251 pull_request_obj = self.__get_pull_request(pull_request_id)
261 pull_request_obj = self.__get_pull_request(pull_request_id)
252 comment.pull_request = pull_request_obj
262 comment.pull_request = pull_request_obj
253 else:
263 else:
254 raise Exception('Please specify commit or pull_request_id')
264 raise Exception('Please specify commit or pull_request_id')
255
265
256 Session().add(comment)
266 Session().add(comment)
257 Session().flush()
267 Session().flush()
258 kwargs = {
268 kwargs = {
259 'user': user,
269 'user': user,
260 'renderer_type': renderer,
270 'renderer_type': renderer,
261 'repo_name': repo.repo_name,
271 'repo_name': repo.repo_name,
262 'status_change': status_change,
272 'status_change': status_change,
263 'status_change_type': status_change_type,
273 'status_change_type': status_change_type,
264 'comment_body': text,
274 'comment_body': text,
265 'comment_file': f_path,
275 'comment_file': f_path,
266 'comment_line': line_no,
276 'comment_line': line_no,
267 'comment_type': comment_type or 'note'
277 'comment_type': comment_type or 'note'
268 }
278 }
269
279
270 if commit_obj:
280 if commit_obj:
271 recipients = ChangesetComment.get_users(
281 recipients = ChangesetComment.get_users(
272 revision=commit_obj.raw_id)
282 revision=commit_obj.raw_id)
273 # add commit author if it's in RhodeCode system
283 # add commit author if it's in RhodeCode system
274 cs_author = User.get_from_cs_author(commit_obj.author)
284 cs_author = User.get_from_cs_author(commit_obj.author)
275 if not cs_author:
285 if not cs_author:
276 # use repo owner if we cannot extract the author correctly
286 # use repo owner if we cannot extract the author correctly
277 cs_author = repo.user
287 cs_author = repo.user
278 recipients += [cs_author]
288 recipients += [cs_author]
279
289
280 commit_comment_url = self.get_url(comment, request=request)
290 commit_comment_url = self.get_url(comment, request=request)
281
291
282 target_repo_url = h.link_to(
292 target_repo_url = h.link_to(
283 repo.repo_name,
293 repo.repo_name,
284 h.route_url('repo_summary', repo_name=repo.repo_name))
294 h.route_url('repo_summary', repo_name=repo.repo_name))
285
295
286 # commit specifics
296 # commit specifics
287 kwargs.update({
297 kwargs.update({
288 'commit': commit_obj,
298 'commit': commit_obj,
289 'commit_message': commit_obj.message,
299 'commit_message': commit_obj.message,
290 'commit_target_repo': target_repo_url,
300 'commit_target_repo': target_repo_url,
291 'commit_comment_url': commit_comment_url,
301 'commit_comment_url': commit_comment_url,
292 })
302 })
293
303
294 elif pull_request_obj:
304 elif pull_request_obj:
295 # get the current participants of this pull request
305 # get the current participants of this pull request
296 recipients = ChangesetComment.get_users(
306 recipients = ChangesetComment.get_users(
297 pull_request_id=pull_request_obj.pull_request_id)
307 pull_request_id=pull_request_obj.pull_request_id)
298 # add pull request author
308 # add pull request author
299 recipients += [pull_request_obj.author]
309 recipients += [pull_request_obj.author]
300
310
301 # add the reviewers to notification
311 # add the reviewers to notification
302 recipients += [x.user for x in pull_request_obj.reviewers]
312 recipients += [x.user for x in pull_request_obj.reviewers]
303
313
304 pr_target_repo = pull_request_obj.target_repo
314 pr_target_repo = pull_request_obj.target_repo
305 pr_source_repo = pull_request_obj.source_repo
315 pr_source_repo = pull_request_obj.source_repo
306
316
307 pr_comment_url = h.route_url(
317 pr_comment_url = h.route_url(
308 'pullrequest_show',
318 'pullrequest_show',
309 repo_name=pr_target_repo.repo_name,
319 repo_name=pr_target_repo.repo_name,
310 pull_request_id=pull_request_obj.pull_request_id,
320 pull_request_id=pull_request_obj.pull_request_id,
311 _anchor='comment-%s' % comment.comment_id)
321 _anchor='comment-%s' % comment.comment_id)
312
322
313 # set some variables for email notification
323 # set some variables for email notification
314 pr_target_repo_url = h.route_url(
324 pr_target_repo_url = h.route_url(
315 'repo_summary', repo_name=pr_target_repo.repo_name)
325 'repo_summary', repo_name=pr_target_repo.repo_name)
316
326
317 pr_source_repo_url = h.route_url(
327 pr_source_repo_url = h.route_url(
318 'repo_summary', repo_name=pr_source_repo.repo_name)
328 'repo_summary', repo_name=pr_source_repo.repo_name)
319
329
320 # pull request specifics
330 # pull request specifics
321 kwargs.update({
331 kwargs.update({
322 'pull_request': pull_request_obj,
332 'pull_request': pull_request_obj,
323 'pr_id': pull_request_obj.pull_request_id,
333 'pr_id': pull_request_obj.pull_request_id,
324 'pr_target_repo': pr_target_repo,
334 'pr_target_repo': pr_target_repo,
325 'pr_target_repo_url': pr_target_repo_url,
335 'pr_target_repo_url': pr_target_repo_url,
326 'pr_source_repo': pr_source_repo,
336 'pr_source_repo': pr_source_repo,
327 'pr_source_repo_url': pr_source_repo_url,
337 'pr_source_repo_url': pr_source_repo_url,
328 'pr_comment_url': pr_comment_url,
338 'pr_comment_url': pr_comment_url,
329 'pr_closing': closing_pr,
339 'pr_closing': closing_pr,
330 })
340 })
331 if send_email:
341 if send_email:
332 # pre-generate the subject for notification itself
342 # pre-generate the subject for notification itself
333 (subject,
343 (subject,
334 _h, _e, # we don't care about those
344 _h, _e, # we don't care about those
335 body_plaintext) = EmailNotificationModel().render_email(
345 body_plaintext) = EmailNotificationModel().render_email(
336 notification_type, **kwargs)
346 notification_type, **kwargs)
337
347
338 mention_recipients = set(
348 mention_recipients = set(
339 self._extract_mentions(text)).difference(recipients)
349 self._extract_mentions(text)).difference(recipients)
340
350
341 # create notification objects, and emails
351 # create notification objects, and emails
342 NotificationModel().create(
352 NotificationModel().create(
343 created_by=user,
353 created_by=user,
344 notification_subject=subject,
354 notification_subject=subject,
345 notification_body=body_plaintext,
355 notification_body=body_plaintext,
346 notification_type=notification_type,
356 notification_type=notification_type,
347 recipients=recipients,
357 recipients=recipients,
348 mention_recipients=mention_recipients,
358 mention_recipients=mention_recipients,
349 email_kwargs=kwargs,
359 email_kwargs=kwargs,
350 )
360 )
351
361
352 Session().flush()
362 Session().flush()
353 if comment.pull_request:
363 if comment.pull_request:
354 action = 'repo.pull_request.comment.create'
364 action = 'repo.pull_request.comment.create'
355 else:
365 else:
356 action = 'repo.commit.comment.create'
366 action = 'repo.commit.comment.create'
357
367
358 comment_data = comment.get_api_data()
368 comment_data = comment.get_api_data()
359 self._log_audit_action(
369 self._log_audit_action(
360 action, {'data': comment_data}, auth_user, comment)
370 action, {'data': comment_data}, auth_user, comment)
361
371
362 msg_url = ''
372 msg_url = ''
363 channel = None
373 channel = None
364 if commit_obj:
374 if commit_obj:
365 msg_url = commit_comment_url
375 msg_url = commit_comment_url
366 repo_name = repo.repo_name
376 repo_name = repo.repo_name
367 channel = u'/repo${}$/commit/{}'.format(
377 channel = u'/repo${}$/commit/{}'.format(
368 repo_name,
378 repo_name,
369 commit_obj.raw_id
379 commit_obj.raw_id
370 )
380 )
371 elif pull_request_obj:
381 elif pull_request_obj:
372 msg_url = pr_comment_url
382 msg_url = pr_comment_url
373 repo_name = pr_target_repo.repo_name
383 repo_name = pr_target_repo.repo_name
374 channel = u'/repo${}$/pr/{}'.format(
384 channel = u'/repo${}$/pr/{}'.format(
375 repo_name,
385 repo_name,
376 pull_request_id
386 pull_request_id
377 )
387 )
378
388
379 message = '<strong>{}</strong> {} - ' \
389 message = '<strong>{}</strong> {} - ' \
380 '<a onclick="window.location=\'{}\';' \
390 '<a onclick="window.location=\'{}\';' \
381 'window.location.reload()">' \
391 'window.location.reload()">' \
382 '<strong>{}</strong></a>'
392 '<strong>{}</strong></a>'
383 message = message.format(
393 message = message.format(
384 user.username, _('made a comment'), msg_url,
394 user.username, _('made a comment'), msg_url,
385 _('Show it now'))
395 _('Show it now'))
386
396
387 channelstream.post_message(
397 channelstream.post_message(
388 channel, message, user.username,
398 channel, message, user.username,
389 registry=get_current_registry())
399 registry=get_current_registry())
390
400
391 return comment
401 return comment
392
402
393 def delete(self, comment, auth_user):
403 def delete(self, comment, auth_user):
394 """
404 """
395 Deletes given comment
405 Deletes given comment
396 """
406 """
397 comment = self.__get_commit_comment(comment)
407 comment = self.__get_commit_comment(comment)
398 old_data = comment.get_api_data()
408 old_data = comment.get_api_data()
399 Session().delete(comment)
409 Session().delete(comment)
400
410
401 if comment.pull_request:
411 if comment.pull_request:
402 action = 'repo.pull_request.comment.delete'
412 action = 'repo.pull_request.comment.delete'
403 else:
413 else:
404 action = 'repo.commit.comment.delete'
414 action = 'repo.commit.comment.delete'
405
415
406 self._log_audit_action(
416 self._log_audit_action(
407 action, {'old_data': old_data}, auth_user, comment)
417 action, {'old_data': old_data}, auth_user, comment)
408
418
409 return comment
419 return comment
410
420
411 def get_all_comments(self, repo_id, revision=None, pull_request=None):
421 def get_all_comments(self, repo_id, revision=None, pull_request=None):
412 q = ChangesetComment.query()\
422 q = ChangesetComment.query()\
413 .filter(ChangesetComment.repo_id == repo_id)
423 .filter(ChangesetComment.repo_id == repo_id)
414 if revision:
424 if revision:
415 q = q.filter(ChangesetComment.revision == revision)
425 q = q.filter(ChangesetComment.revision == revision)
416 elif pull_request:
426 elif pull_request:
417 pull_request = self.__get_pull_request(pull_request)
427 pull_request = self.__get_pull_request(pull_request)
418 q = q.filter(ChangesetComment.pull_request == pull_request)
428 q = q.filter(ChangesetComment.pull_request == pull_request)
419 else:
429 else:
420 raise Exception('Please specify commit or pull_request')
430 raise Exception('Please specify commit or pull_request')
421 q = q.order_by(ChangesetComment.created_on)
431 q = q.order_by(ChangesetComment.created_on)
422 return q.all()
432 return q.all()
423
433
424 def get_url(self, comment, request=None, permalink=False):
434 def get_url(self, comment, request=None, permalink=False):
425 if not request:
435 if not request:
426 request = get_current_request()
436 request = get_current_request()
427
437
428 comment = self.__get_commit_comment(comment)
438 comment = self.__get_commit_comment(comment)
429 if comment.pull_request:
439 if comment.pull_request:
430 pull_request = comment.pull_request
440 pull_request = comment.pull_request
431 if permalink:
441 if permalink:
432 return request.route_url(
442 return request.route_url(
433 'pull_requests_global',
443 'pull_requests_global',
434 pull_request_id=pull_request.pull_request_id,
444 pull_request_id=pull_request.pull_request_id,
435 _anchor='comment-%s' % comment.comment_id)
445 _anchor='comment-%s' % comment.comment_id)
436 else:
446 else:
437 return request.route_url(
447 return request.route_url(
438 'pullrequest_show',
448 'pullrequest_show',
439 repo_name=safe_str(pull_request.target_repo.repo_name),
449 repo_name=safe_str(pull_request.target_repo.repo_name),
440 pull_request_id=pull_request.pull_request_id,
450 pull_request_id=pull_request.pull_request_id,
441 _anchor='comment-%s' % comment.comment_id)
451 _anchor='comment-%s' % comment.comment_id)
442
452
443 else:
453 else:
444 repo = comment.repo
454 repo = comment.repo
445 commit_id = comment.revision
455 commit_id = comment.revision
446
456
447 if permalink:
457 if permalink:
448 return request.route_url(
458 return request.route_url(
449 'repo_commit', repo_name=safe_str(repo.repo_id),
459 'repo_commit', repo_name=safe_str(repo.repo_id),
450 commit_id=commit_id,
460 commit_id=commit_id,
451 _anchor='comment-%s' % comment.comment_id)
461 _anchor='comment-%s' % comment.comment_id)
452
462
453 else:
463 else:
454 return request.route_url(
464 return request.route_url(
455 'repo_commit', repo_name=safe_str(repo.repo_name),
465 'repo_commit', repo_name=safe_str(repo.repo_name),
456 commit_id=commit_id,
466 commit_id=commit_id,
457 _anchor='comment-%s' % comment.comment_id)
467 _anchor='comment-%s' % comment.comment_id)
458
468
459 def get_comments(self, repo_id, revision=None, pull_request=None):
469 def get_comments(self, repo_id, revision=None, pull_request=None):
460 """
470 """
461 Gets main comments based on revision or pull_request_id
471 Gets main comments based on revision or pull_request_id
462
472
463 :param repo_id:
473 :param repo_id:
464 :param revision:
474 :param revision:
465 :param pull_request:
475 :param pull_request:
466 """
476 """
467
477
468 q = ChangesetComment.query()\
478 q = ChangesetComment.query()\
469 .filter(ChangesetComment.repo_id == repo_id)\
479 .filter(ChangesetComment.repo_id == repo_id)\
470 .filter(ChangesetComment.line_no == None)\
480 .filter(ChangesetComment.line_no == None)\
471 .filter(ChangesetComment.f_path == None)
481 .filter(ChangesetComment.f_path == None)
472 if revision:
482 if revision:
473 q = q.filter(ChangesetComment.revision == revision)
483 q = q.filter(ChangesetComment.revision == revision)
474 elif pull_request:
484 elif pull_request:
475 pull_request = self.__get_pull_request(pull_request)
485 pull_request = self.__get_pull_request(pull_request)
476 q = q.filter(ChangesetComment.pull_request == pull_request)
486 q = q.filter(ChangesetComment.pull_request == pull_request)
477 else:
487 else:
478 raise Exception('Please specify commit or pull_request')
488 raise Exception('Please specify commit or pull_request')
479 q = q.order_by(ChangesetComment.created_on)
489 q = q.order_by(ChangesetComment.created_on)
480 return q.all()
490 return q.all()
481
491
482 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
492 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
483 q = self._get_inline_comments_query(repo_id, revision, pull_request)
493 q = self._get_inline_comments_query(repo_id, revision, pull_request)
484 return self._group_comments_by_path_and_line_number(q)
494 return self._group_comments_by_path_and_line_number(q)
485
495
486 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
496 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
487 version=None):
497 version=None):
488 inline_cnt = 0
498 inline_cnt = 0
489 for fname, per_line_comments in inline_comments.iteritems():
499 for fname, per_line_comments in inline_comments.iteritems():
490 for lno, comments in per_line_comments.iteritems():
500 for lno, comments in per_line_comments.iteritems():
491 for comm in comments:
501 for comm in comments:
492 if not comm.outdated_at_version(version) and skip_outdated:
502 if not comm.outdated_at_version(version) and skip_outdated:
493 inline_cnt += 1
503 inline_cnt += 1
494
504
495 return inline_cnt
505 return inline_cnt
496
506
497 def get_outdated_comments(self, repo_id, pull_request):
507 def get_outdated_comments(self, repo_id, pull_request):
498 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
508 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
499 # of a pull request.
509 # of a pull request.
500 q = self._all_inline_comments_of_pull_request(pull_request)
510 q = self._all_inline_comments_of_pull_request(pull_request)
501 q = q.filter(
511 q = q.filter(
502 ChangesetComment.display_state ==
512 ChangesetComment.display_state ==
503 ChangesetComment.COMMENT_OUTDATED
513 ChangesetComment.COMMENT_OUTDATED
504 ).order_by(ChangesetComment.comment_id.asc())
514 ).order_by(ChangesetComment.comment_id.asc())
505
515
506 return self._group_comments_by_path_and_line_number(q)
516 return self._group_comments_by_path_and_line_number(q)
507
517
508 def _get_inline_comments_query(self, repo_id, revision, pull_request):
518 def _get_inline_comments_query(self, repo_id, revision, pull_request):
509 # TODO: johbo: Split this into two methods: One for PR and one for
519 # TODO: johbo: Split this into two methods: One for PR and one for
510 # commit.
520 # commit.
511 if revision:
521 if revision:
512 q = Session().query(ChangesetComment).filter(
522 q = Session().query(ChangesetComment).filter(
513 ChangesetComment.repo_id == repo_id,
523 ChangesetComment.repo_id == repo_id,
514 ChangesetComment.line_no != null(),
524 ChangesetComment.line_no != null(),
515 ChangesetComment.f_path != null(),
525 ChangesetComment.f_path != null(),
516 ChangesetComment.revision == revision)
526 ChangesetComment.revision == revision)
517
527
518 elif pull_request:
528 elif pull_request:
519 pull_request = self.__get_pull_request(pull_request)
529 pull_request = self.__get_pull_request(pull_request)
520 if not CommentsModel.use_outdated_comments(pull_request):
530 if not CommentsModel.use_outdated_comments(pull_request):
521 q = self._visible_inline_comments_of_pull_request(pull_request)
531 q = self._visible_inline_comments_of_pull_request(pull_request)
522 else:
532 else:
523 q = self._all_inline_comments_of_pull_request(pull_request)
533 q = self._all_inline_comments_of_pull_request(pull_request)
524
534
525 else:
535 else:
526 raise Exception('Please specify commit or pull_request_id')
536 raise Exception('Please specify commit or pull_request_id')
527 q = q.order_by(ChangesetComment.comment_id.asc())
537 q = q.order_by(ChangesetComment.comment_id.asc())
528 return q
538 return q
529
539
530 def _group_comments_by_path_and_line_number(self, q):
540 def _group_comments_by_path_and_line_number(self, q):
531 comments = q.all()
541 comments = q.all()
532 paths = collections.defaultdict(lambda: collections.defaultdict(list))
542 paths = collections.defaultdict(lambda: collections.defaultdict(list))
533 for co in comments:
543 for co in comments:
534 paths[co.f_path][co.line_no].append(co)
544 paths[co.f_path][co.line_no].append(co)
535 return paths
545 return paths
536
546
537 @classmethod
547 @classmethod
538 def needed_extra_diff_context(cls):
548 def needed_extra_diff_context(cls):
539 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
549 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
540
550
541 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
551 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
542 if not CommentsModel.use_outdated_comments(pull_request):
552 if not CommentsModel.use_outdated_comments(pull_request):
543 return
553 return
544
554
545 comments = self._visible_inline_comments_of_pull_request(pull_request)
555 comments = self._visible_inline_comments_of_pull_request(pull_request)
546 comments_to_outdate = comments.all()
556 comments_to_outdate = comments.all()
547
557
548 for comment in comments_to_outdate:
558 for comment in comments_to_outdate:
549 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
559 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
550
560
551 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
561 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
552 diff_line = _parse_comment_line_number(comment.line_no)
562 diff_line = _parse_comment_line_number(comment.line_no)
553
563
554 try:
564 try:
555 old_context = old_diff_proc.get_context_of_line(
565 old_context = old_diff_proc.get_context_of_line(
556 path=comment.f_path, diff_line=diff_line)
566 path=comment.f_path, diff_line=diff_line)
557 new_context = new_diff_proc.get_context_of_line(
567 new_context = new_diff_proc.get_context_of_line(
558 path=comment.f_path, diff_line=diff_line)
568 path=comment.f_path, diff_line=diff_line)
559 except (diffs.LineNotInDiffException,
569 except (diffs.LineNotInDiffException,
560 diffs.FileNotInDiffException):
570 diffs.FileNotInDiffException):
561 comment.display_state = ChangesetComment.COMMENT_OUTDATED
571 comment.display_state = ChangesetComment.COMMENT_OUTDATED
562 return
572 return
563
573
564 if old_context == new_context:
574 if old_context == new_context:
565 return
575 return
566
576
567 if self._should_relocate_diff_line(diff_line):
577 if self._should_relocate_diff_line(diff_line):
568 new_diff_lines = new_diff_proc.find_context(
578 new_diff_lines = new_diff_proc.find_context(
569 path=comment.f_path, context=old_context,
579 path=comment.f_path, context=old_context,
570 offset=self.DIFF_CONTEXT_BEFORE)
580 offset=self.DIFF_CONTEXT_BEFORE)
571 if not new_diff_lines:
581 if not new_diff_lines:
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
582 comment.display_state = ChangesetComment.COMMENT_OUTDATED
573 else:
583 else:
574 new_diff_line = self._choose_closest_diff_line(
584 new_diff_line = self._choose_closest_diff_line(
575 diff_line, new_diff_lines)
585 diff_line, new_diff_lines)
576 comment.line_no = _diff_to_comment_line_number(new_diff_line)
586 comment.line_no = _diff_to_comment_line_number(new_diff_line)
577 else:
587 else:
578 comment.display_state = ChangesetComment.COMMENT_OUTDATED
588 comment.display_state = ChangesetComment.COMMENT_OUTDATED
579
589
580 def _should_relocate_diff_line(self, diff_line):
590 def _should_relocate_diff_line(self, diff_line):
581 """
591 """
582 Checks if relocation shall be tried for the given `diff_line`.
592 Checks if relocation shall be tried for the given `diff_line`.
583
593
584 If a comment points into the first lines, then we can have a situation
594 If a comment points into the first lines, then we can have a situation
585 that after an update another line has been added on top. In this case
595 that after an update another line has been added on top. In this case
586 we would find the context still and move the comment around. This
596 we would find the context still and move the comment around. This
587 would be wrong.
597 would be wrong.
588 """
598 """
589 should_relocate = (
599 should_relocate = (
590 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
600 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
591 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
601 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
592 return should_relocate
602 return should_relocate
593
603
594 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
604 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
595 candidate = new_diff_lines[0]
605 candidate = new_diff_lines[0]
596 best_delta = _diff_line_delta(diff_line, candidate)
606 best_delta = _diff_line_delta(diff_line, candidate)
597 for new_diff_line in new_diff_lines[1:]:
607 for new_diff_line in new_diff_lines[1:]:
598 delta = _diff_line_delta(diff_line, new_diff_line)
608 delta = _diff_line_delta(diff_line, new_diff_line)
599 if delta < best_delta:
609 if delta < best_delta:
600 candidate = new_diff_line
610 candidate = new_diff_line
601 best_delta = delta
611 best_delta = delta
602 return candidate
612 return candidate
603
613
604 def _visible_inline_comments_of_pull_request(self, pull_request):
614 def _visible_inline_comments_of_pull_request(self, pull_request):
605 comments = self._all_inline_comments_of_pull_request(pull_request)
615 comments = self._all_inline_comments_of_pull_request(pull_request)
606 comments = comments.filter(
616 comments = comments.filter(
607 coalesce(ChangesetComment.display_state, '') !=
617 coalesce(ChangesetComment.display_state, '') !=
608 ChangesetComment.COMMENT_OUTDATED)
618 ChangesetComment.COMMENT_OUTDATED)
609 return comments
619 return comments
610
620
611 def _all_inline_comments_of_pull_request(self, pull_request):
621 def _all_inline_comments_of_pull_request(self, pull_request):
612 comments = Session().query(ChangesetComment)\
622 comments = Session().query(ChangesetComment)\
613 .filter(ChangesetComment.line_no != None)\
623 .filter(ChangesetComment.line_no != None)\
614 .filter(ChangesetComment.f_path != None)\
624 .filter(ChangesetComment.f_path != None)\
615 .filter(ChangesetComment.pull_request == pull_request)
625 .filter(ChangesetComment.pull_request == pull_request)
616 return comments
626 return comments
617
627
618 def _all_general_comments_of_pull_request(self, pull_request):
628 def _all_general_comments_of_pull_request(self, pull_request):
619 comments = Session().query(ChangesetComment)\
629 comments = Session().query(ChangesetComment)\
620 .filter(ChangesetComment.line_no == None)\
630 .filter(ChangesetComment.line_no == None)\
621 .filter(ChangesetComment.f_path == None)\
631 .filter(ChangesetComment.f_path == None)\
622 .filter(ChangesetComment.pull_request == pull_request)
632 .filter(ChangesetComment.pull_request == pull_request)
623 return comments
633 return comments
624
634
625 @staticmethod
635 @staticmethod
626 def use_outdated_comments(pull_request):
636 def use_outdated_comments(pull_request):
627 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
637 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
628 settings = settings_model.get_general_settings()
638 settings = settings_model.get_general_settings()
629 return settings.get('rhodecode_use_outdated_comments', False)
639 return settings.get('rhodecode_use_outdated_comments', False)
630
640
631
641
632 def _parse_comment_line_number(line_no):
642 def _parse_comment_line_number(line_no):
633 """
643 """
634 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
644 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
635 """
645 """
636 old_line = None
646 old_line = None
637 new_line = None
647 new_line = None
638 if line_no.startswith('o'):
648 if line_no.startswith('o'):
639 old_line = int(line_no[1:])
649 old_line = int(line_no[1:])
640 elif line_no.startswith('n'):
650 elif line_no.startswith('n'):
641 new_line = int(line_no[1:])
651 new_line = int(line_no[1:])
642 else:
652 else:
643 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
653 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
644 return diffs.DiffLineNumber(old_line, new_line)
654 return diffs.DiffLineNumber(old_line, new_line)
645
655
646
656
647 def _diff_to_comment_line_number(diff_line):
657 def _diff_to_comment_line_number(diff_line):
648 if diff_line.new is not None:
658 if diff_line.new is not None:
649 return u'n{}'.format(diff_line.new)
659 return u'n{}'.format(diff_line.new)
650 elif diff_line.old is not None:
660 elif diff_line.old is not None:
651 return u'o{}'.format(diff_line.old)
661 return u'o{}'.format(diff_line.old)
652 return u''
662 return u''
653
663
654
664
655 def _diff_line_delta(a, b):
665 def _diff_line_delta(a, b):
656 if None not in (a.new, b.new):
666 if None not in (a.new, b.new):
657 return abs(a.new - b.new)
667 return abs(a.new - b.new)
658 elif None not in (a.old, b.old):
668 elif None not in (a.old, b.old):
659 return abs(a.old - b.old)
669 return abs(a.old - b.old)
660 else:
670 else:
661 raise ValueError(
671 raise ValueError(
662 "Cannot compute delta between {} and {}".format(a, b))
672 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1715 +1,1715 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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 datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid import compat
33 from pyramid import compat
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78
78
79 UPDATE_STATUS_MESSAGES = {
79 UPDATE_STATUS_MESSAGES = {
80 UpdateFailureReason.NONE: lazy_ugettext(
80 UpdateFailureReason.NONE: lazy_ugettext(
81 'Pull request update successful.'),
81 'Pull request update successful.'),
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 'Pull request update failed because of an unknown error.'),
83 'Pull request update failed because of an unknown error.'),
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 'No update needed because the source and target have not changed.'),
85 'No update needed because the source and target have not changed.'),
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 'Pull request cannot be updated because the reference type is '
87 'Pull request cannot be updated because the reference type is '
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 'This pull request cannot be updated because the target '
90 'This pull request cannot be updated because the target '
91 'reference is missing.'),
91 'reference is missing.'),
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 'This pull request cannot be updated because the source '
93 'This pull request cannot be updated because the source '
94 'reference is missing.'),
94 'reference is missing.'),
95 }
95 }
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98
98
99 def __get_pull_request(self, pull_request):
99 def __get_pull_request(self, pull_request):
100 return self._get_instance((
100 return self._get_instance((
101 PullRequest, PullRequestVersion), pull_request)
101 PullRequest, PullRequestVersion), pull_request)
102
102
103 def _check_perms(self, perms, pull_request, user, api=False):
103 def _check_perms(self, perms, pull_request, user, api=False):
104 if not api:
104 if not api:
105 return h.HasRepoPermissionAny(*perms)(
105 return h.HasRepoPermissionAny(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107 else:
107 else:
108 return h.HasRepoPermissionAnyApi(*perms)(
108 return h.HasRepoPermissionAnyApi(*perms)(
109 user=user, repo_name=pull_request.target_repo.repo_name)
109 user=user, repo_name=pull_request.target_repo.repo_name)
110
110
111 def check_user_read(self, pull_request, user, api=False):
111 def check_user_read(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_merge(self, pull_request, user, api=False):
115 def check_user_merge(self, pull_request, user, api=False):
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 return self._check_perms(_perms, pull_request, user, api)
117 return self._check_perms(_perms, pull_request, user, api)
118
118
119 def check_user_update(self, pull_request, user, api=False):
119 def check_user_update(self, pull_request, user, api=False):
120 owner = user.user_id == pull_request.user_id
120 owner = user.user_id == pull_request.user_id
121 return self.check_user_merge(pull_request, user, api) or owner
121 return self.check_user_merge(pull_request, user, api) or owner
122
122
123 def check_user_delete(self, pull_request, user):
123 def check_user_delete(self, pull_request, user):
124 owner = user.user_id == pull_request.user_id
124 owner = user.user_id == pull_request.user_id
125 _perms = ('repository.admin',)
125 _perms = ('repository.admin',)
126 return self._check_perms(_perms, pull_request, user) or owner
126 return self._check_perms(_perms, pull_request, user) or owner
127
127
128 def check_user_change_status(self, pull_request, user, api=False):
128 def check_user_change_status(self, pull_request, user, api=False):
129 reviewer = user.user_id in [x.user_id for x in
129 reviewer = user.user_id in [x.user_id for x in
130 pull_request.reviewers]
130 pull_request.reviewers]
131 return self.check_user_update(pull_request, user, api) or reviewer
131 return self.check_user_update(pull_request, user, api) or reviewer
132
132
133 def check_user_comment(self, pull_request, user):
133 def check_user_comment(self, pull_request, user):
134 owner = user.user_id == pull_request.user_id
134 owner = user.user_id == pull_request.user_id
135 return self.check_user_read(pull_request, user) or owner
135 return self.check_user_read(pull_request, user) or owner
136
136
137 def get(self, pull_request):
137 def get(self, pull_request):
138 return self.__get_pull_request(pull_request)
138 return self.__get_pull_request(pull_request)
139
139
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 opened_by=None, order_by=None,
141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=True):
142 order_dir='desc', only_created=True):
143 repo = None
143 repo = None
144 if repo_name:
144 if repo_name:
145 repo = self._get_repo(repo_name)
145 repo = self._get_repo(repo_name)
146
146
147 q = PullRequest.query()
147 q = PullRequest.query()
148
148
149 # source or target
149 # source or target
150 if repo and source:
150 if repo and source:
151 q = q.filter(PullRequest.source_repo == repo)
151 q = q.filter(PullRequest.source_repo == repo)
152 elif repo:
152 elif repo:
153 q = q.filter(PullRequest.target_repo == repo)
153 q = q.filter(PullRequest.target_repo == repo)
154
154
155 # closed,opened
155 # closed,opened
156 if statuses:
156 if statuses:
157 q = q.filter(PullRequest.status.in_(statuses))
157 q = q.filter(PullRequest.status.in_(statuses))
158
158
159 # opened by filter
159 # opened by filter
160 if opened_by:
160 if opened_by:
161 q = q.filter(PullRequest.user_id.in_(opened_by))
161 q = q.filter(PullRequest.user_id.in_(opened_by))
162
162
163 # only get those that are in "created" state
163 # only get those that are in "created" state
164 if only_created:
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
166
167 if order_by:
167 if order_by:
168 order_map = {
168 order_map = {
169 'name_raw': PullRequest.pull_request_id,
169 'name_raw': PullRequest.pull_request_id,
170 'title': PullRequest.title,
170 'title': PullRequest.title,
171 'updated_on_raw': PullRequest.updated_on,
171 'updated_on_raw': PullRequest.updated_on,
172 'target_repo': PullRequest.target_repo_id
172 'target_repo': PullRequest.target_repo_id
173 }
173 }
174 if order_dir == 'asc':
174 if order_dir == 'asc':
175 q = q.order_by(order_map[order_by].asc())
175 q = q.order_by(order_map[order_by].asc())
176 else:
176 else:
177 q = q.order_by(order_map[order_by].desc())
177 q = q.order_by(order_map[order_by].desc())
178
178
179 return q
179 return q
180
180
181 def count_all(self, repo_name, source=False, statuses=None,
181 def count_all(self, repo_name, source=False, statuses=None,
182 opened_by=None):
182 opened_by=None):
183 """
183 """
184 Count the number of pull requests for a specific repository.
184 Count the number of pull requests for a specific repository.
185
185
186 :param repo_name: target or source repo
186 :param repo_name: target or source repo
187 :param source: boolean flag to specify if repo_name refers to source
187 :param source: boolean flag to specify if repo_name refers to source
188 :param statuses: list of pull request statuses
188 :param statuses: list of pull request statuses
189 :param opened_by: author user of the pull request
189 :param opened_by: author user of the pull request
190 :returns: int number of pull requests
190 :returns: int number of pull requests
191 """
191 """
192 q = self._prepare_get_all_query(
192 q = self._prepare_get_all_query(
193 repo_name, source=source, statuses=statuses, opened_by=opened_by)
193 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194
194
195 return q.count()
195 return q.count()
196
196
197 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
197 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 offset=0, length=None, order_by=None, order_dir='desc'):
198 offset=0, length=None, order_by=None, order_dir='desc'):
199 """
199 """
200 Get all pull requests for a specific repository.
200 Get all pull requests for a specific repository.
201
201
202 :param repo_name: target or source repo
202 :param repo_name: target or source repo
203 :param source: boolean flag to specify if repo_name refers to source
203 :param source: boolean flag to specify if repo_name refers to source
204 :param statuses: list of pull request statuses
204 :param statuses: list of pull request statuses
205 :param opened_by: author user of the pull request
205 :param opened_by: author user of the pull request
206 :param offset: pagination offset
206 :param offset: pagination offset
207 :param length: length of returned list
207 :param length: length of returned list
208 :param order_by: order of the returned list
208 :param order_by: order of the returned list
209 :param order_dir: 'asc' or 'desc' ordering direction
209 :param order_dir: 'asc' or 'desc' ordering direction
210 :returns: list of pull requests
210 :returns: list of pull requests
211 """
211 """
212 q = self._prepare_get_all_query(
212 q = self._prepare_get_all_query(
213 repo_name, source=source, statuses=statuses, opened_by=opened_by,
213 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 order_by=order_by, order_dir=order_dir)
214 order_by=order_by, order_dir=order_dir)
215
215
216 if length:
216 if length:
217 pull_requests = q.limit(length).offset(offset).all()
217 pull_requests = q.limit(length).offset(offset).all()
218 else:
218 else:
219 pull_requests = q.all()
219 pull_requests = q.all()
220
220
221 return pull_requests
221 return pull_requests
222
222
223 def count_awaiting_review(self, repo_name, source=False, statuses=None,
223 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 opened_by=None):
224 opened_by=None):
225 """
225 """
226 Count the number of pull requests for a specific repository that are
226 Count the number of pull requests for a specific repository that are
227 awaiting review.
227 awaiting review.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :returns: int number of pull requests
233 :returns: int number of pull requests
234 """
234 """
235 pull_requests = self.get_awaiting_review(
235 pull_requests = self.get_awaiting_review(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by)
236 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237
237
238 return len(pull_requests)
238 return len(pull_requests)
239
239
240 def get_awaiting_review(self, repo_name, source=False, statuses=None,
240 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 opened_by=None, offset=0, length=None,
241 opened_by=None, offset=0, length=None,
242 order_by=None, order_dir='desc'):
242 order_by=None, order_dir='desc'):
243 """
243 """
244 Get all pull requests for a specific repository that are awaiting
244 Get all pull requests for a specific repository that are awaiting
245 review.
245 review.
246
246
247 :param repo_name: target or source repo
247 :param repo_name: target or source repo
248 :param source: boolean flag to specify if repo_name refers to source
248 :param source: boolean flag to specify if repo_name refers to source
249 :param statuses: list of pull request statuses
249 :param statuses: list of pull request statuses
250 :param opened_by: author user of the pull request
250 :param opened_by: author user of the pull request
251 :param offset: pagination offset
251 :param offset: pagination offset
252 :param length: length of returned list
252 :param length: length of returned list
253 :param order_by: order of the returned list
253 :param order_by: order of the returned list
254 :param order_dir: 'asc' or 'desc' ordering direction
254 :param order_dir: 'asc' or 'desc' ordering direction
255 :returns: list of pull requests
255 :returns: list of pull requests
256 """
256 """
257 pull_requests = self.get_all(
257 pull_requests = self.get_all(
258 repo_name, source=source, statuses=statuses, opened_by=opened_by,
258 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 order_by=order_by, order_dir=order_dir)
259 order_by=order_by, order_dir=order_dir)
260
260
261 _filtered_pull_requests = []
261 _filtered_pull_requests = []
262 for pr in pull_requests:
262 for pr in pull_requests:
263 status = pr.calculated_review_status()
263 status = pr.calculated_review_status()
264 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
264 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 ChangesetStatus.STATUS_UNDER_REVIEW]:
265 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 _filtered_pull_requests.append(pr)
266 _filtered_pull_requests.append(pr)
267 if length:
267 if length:
268 return _filtered_pull_requests[offset:offset+length]
268 return _filtered_pull_requests[offset:offset+length]
269 else:
269 else:
270 return _filtered_pull_requests
270 return _filtered_pull_requests
271
271
272 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
272 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 opened_by=None, user_id=None):
273 opened_by=None, user_id=None):
274 """
274 """
275 Count the number of pull requests for a specific repository that are
275 Count the number of pull requests for a specific repository that are
276 awaiting review from a specific user.
276 awaiting review from a specific user.
277
277
278 :param repo_name: target or source repo
278 :param repo_name: target or source repo
279 :param source: boolean flag to specify if repo_name refers to source
279 :param source: boolean flag to specify if repo_name refers to source
280 :param statuses: list of pull request statuses
280 :param statuses: list of pull request statuses
281 :param opened_by: author user of the pull request
281 :param opened_by: author user of the pull request
282 :param user_id: reviewer user of the pull request
282 :param user_id: reviewer user of the pull request
283 :returns: int number of pull requests
283 :returns: int number of pull requests
284 """
284 """
285 pull_requests = self.get_awaiting_my_review(
285 pull_requests = self.get_awaiting_my_review(
286 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 user_id=user_id)
287 user_id=user_id)
288
288
289 return len(pull_requests)
289 return len(pull_requests)
290
290
291 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
291 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 opened_by=None, user_id=None, offset=0,
292 opened_by=None, user_id=None, offset=0,
293 length=None, order_by=None, order_dir='desc'):
293 length=None, order_by=None, order_dir='desc'):
294 """
294 """
295 Get all pull requests for a specific repository that are awaiting
295 Get all pull requests for a specific repository that are awaiting
296 review from a specific user.
296 review from a specific user.
297
297
298 :param repo_name: target or source repo
298 :param repo_name: target or source repo
299 :param source: boolean flag to specify if repo_name refers to source
299 :param source: boolean flag to specify if repo_name refers to source
300 :param statuses: list of pull request statuses
300 :param statuses: list of pull request statuses
301 :param opened_by: author user of the pull request
301 :param opened_by: author user of the pull request
302 :param user_id: reviewer user of the pull request
302 :param user_id: reviewer user of the pull request
303 :param offset: pagination offset
303 :param offset: pagination offset
304 :param length: length of returned list
304 :param length: length of returned list
305 :param order_by: order of the returned list
305 :param order_by: order of the returned list
306 :param order_dir: 'asc' or 'desc' ordering direction
306 :param order_dir: 'asc' or 'desc' ordering direction
307 :returns: list of pull requests
307 :returns: list of pull requests
308 """
308 """
309 pull_requests = self.get_all(
309 pull_requests = self.get_all(
310 repo_name, source=source, statuses=statuses, opened_by=opened_by,
310 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 order_by=order_by, order_dir=order_dir)
311 order_by=order_by, order_dir=order_dir)
312
312
313 _my = PullRequestModel().get_not_reviewed(user_id)
313 _my = PullRequestModel().get_not_reviewed(user_id)
314 my_participation = []
314 my_participation = []
315 for pr in pull_requests:
315 for pr in pull_requests:
316 if pr in _my:
316 if pr in _my:
317 my_participation.append(pr)
317 my_participation.append(pr)
318 _filtered_pull_requests = my_participation
318 _filtered_pull_requests = my_participation
319 if length:
319 if length:
320 return _filtered_pull_requests[offset:offset+length]
320 return _filtered_pull_requests[offset:offset+length]
321 else:
321 else:
322 return _filtered_pull_requests
322 return _filtered_pull_requests
323
323
324 def get_not_reviewed(self, user_id):
324 def get_not_reviewed(self, user_id):
325 return [
325 return [
326 x.pull_request for x in PullRequestReviewers.query().filter(
326 x.pull_request for x in PullRequestReviewers.query().filter(
327 PullRequestReviewers.user_id == user_id).all()
327 PullRequestReviewers.user_id == user_id).all()
328 ]
328 ]
329
329
330 def _prepare_participating_query(self, user_id=None, statuses=None,
330 def _prepare_participating_query(self, user_id=None, statuses=None,
331 order_by=None, order_dir='desc'):
331 order_by=None, order_dir='desc'):
332 q = PullRequest.query()
332 q = PullRequest.query()
333 if user_id:
333 if user_id:
334 reviewers_subquery = Session().query(
334 reviewers_subquery = Session().query(
335 PullRequestReviewers.pull_request_id).filter(
335 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.user_id == user_id).subquery()
336 PullRequestReviewers.user_id == user_id).subquery()
337 user_filter = or_(
337 user_filter = or_(
338 PullRequest.user_id == user_id,
338 PullRequest.user_id == user_id,
339 PullRequest.pull_request_id.in_(reviewers_subquery)
339 PullRequest.pull_request_id.in_(reviewers_subquery)
340 )
340 )
341 q = PullRequest.query().filter(user_filter)
341 q = PullRequest.query().filter(user_filter)
342
342
343 # closed,opened
343 # closed,opened
344 if statuses:
344 if statuses:
345 q = q.filter(PullRequest.status.in_(statuses))
345 q = q.filter(PullRequest.status.in_(statuses))
346
346
347 if order_by:
347 if order_by:
348 order_map = {
348 order_map = {
349 'name_raw': PullRequest.pull_request_id,
349 'name_raw': PullRequest.pull_request_id,
350 'title': PullRequest.title,
350 'title': PullRequest.title,
351 'updated_on_raw': PullRequest.updated_on,
351 'updated_on_raw': PullRequest.updated_on,
352 'target_repo': PullRequest.target_repo_id
352 'target_repo': PullRequest.target_repo_id
353 }
353 }
354 if order_dir == 'asc':
354 if order_dir == 'asc':
355 q = q.order_by(order_map[order_by].asc())
355 q = q.order_by(order_map[order_by].asc())
356 else:
356 else:
357 q = q.order_by(order_map[order_by].desc())
357 q = q.order_by(order_map[order_by].desc())
358
358
359 return q
359 return q
360
360
361 def count_im_participating_in(self, user_id=None, statuses=None):
361 def count_im_participating_in(self, user_id=None, statuses=None):
362 q = self._prepare_participating_query(user_id, statuses=statuses)
362 q = self._prepare_participating_query(user_id, statuses=statuses)
363 return q.count()
363 return q.count()
364
364
365 def get_im_participating_in(
365 def get_im_participating_in(
366 self, user_id=None, statuses=None, offset=0,
366 self, user_id=None, statuses=None, offset=0,
367 length=None, order_by=None, order_dir='desc'):
367 length=None, order_by=None, order_dir='desc'):
368 """
368 """
369 Get all Pull requests that i'm participating in, or i have opened
369 Get all Pull requests that i'm participating in, or i have opened
370 """
370 """
371
371
372 q = self._prepare_participating_query(
372 q = self._prepare_participating_query(
373 user_id, statuses=statuses, order_by=order_by,
373 user_id, statuses=statuses, order_by=order_by,
374 order_dir=order_dir)
374 order_dir=order_dir)
375
375
376 if length:
376 if length:
377 pull_requests = q.limit(length).offset(offset).all()
377 pull_requests = q.limit(length).offset(offset).all()
378 else:
378 else:
379 pull_requests = q.all()
379 pull_requests = q.all()
380
380
381 return pull_requests
381 return pull_requests
382
382
383 def get_versions(self, pull_request):
383 def get_versions(self, pull_request):
384 """
384 """
385 returns version of pull request sorted by ID descending
385 returns version of pull request sorted by ID descending
386 """
386 """
387 return PullRequestVersion.query()\
387 return PullRequestVersion.query()\
388 .filter(PullRequestVersion.pull_request == pull_request)\
388 .filter(PullRequestVersion.pull_request == pull_request)\
389 .order_by(PullRequestVersion.pull_request_version_id.asc())\
389 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .all()
390 .all()
391
391
392 def get_pr_version(self, pull_request_id, version=None):
392 def get_pr_version(self, pull_request_id, version=None):
393 at_version = None
393 at_version = None
394
394
395 if version and version == 'latest':
395 if version and version == 'latest':
396 pull_request_ver = PullRequest.get(pull_request_id)
396 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_obj = pull_request_ver
397 pull_request_obj = pull_request_ver
398 _org_pull_request_obj = pull_request_obj
398 _org_pull_request_obj = pull_request_obj
399 at_version = 'latest'
399 at_version = 'latest'
400 elif version:
400 elif version:
401 pull_request_ver = PullRequestVersion.get_or_404(version)
401 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_obj = pull_request_ver
402 pull_request_obj = pull_request_ver
403 _org_pull_request_obj = pull_request_ver.pull_request
403 _org_pull_request_obj = pull_request_ver.pull_request
404 at_version = pull_request_ver.pull_request_version_id
404 at_version = pull_request_ver.pull_request_version_id
405 else:
405 else:
406 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
406 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 pull_request_id)
407 pull_request_id)
408
408
409 pull_request_display_obj = PullRequest.get_pr_display_object(
409 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_obj, _org_pull_request_obj)
410 pull_request_obj, _org_pull_request_obj)
411
411
412 return _org_pull_request_obj, pull_request_obj, \
412 return _org_pull_request_obj, pull_request_obj, \
413 pull_request_display_obj, at_version
413 pull_request_display_obj, at_version
414
414
415 def create(self, created_by, source_repo, source_ref, target_repo,
415 def create(self, created_by, source_repo, source_ref, target_repo,
416 target_ref, revisions, reviewers, title, description=None,
416 target_ref, revisions, reviewers, title, description=None,
417 description_renderer=None,
417 description_renderer=None,
418 reviewer_data=None, translator=None, auth_user=None):
418 reviewer_data=None, translator=None, auth_user=None):
419 translator = translator or get_current_request().translate
419 translator = translator or get_current_request().translate
420
420
421 created_by_user = self._get_user(created_by)
421 created_by_user = self._get_user(created_by)
422 auth_user = auth_user or created_by_user.AuthUser()
422 auth_user = auth_user or created_by_user.AuthUser()
423 source_repo = self._get_repo(source_repo)
423 source_repo = self._get_repo(source_repo)
424 target_repo = self._get_repo(target_repo)
424 target_repo = self._get_repo(target_repo)
425
425
426 pull_request = PullRequest()
426 pull_request = PullRequest()
427 pull_request.source_repo = source_repo
427 pull_request.source_repo = source_repo
428 pull_request.source_ref = source_ref
428 pull_request.source_ref = source_ref
429 pull_request.target_repo = target_repo
429 pull_request.target_repo = target_repo
430 pull_request.target_ref = target_ref
430 pull_request.target_ref = target_ref
431 pull_request.revisions = revisions
431 pull_request.revisions = revisions
432 pull_request.title = title
432 pull_request.title = title
433 pull_request.description = description
433 pull_request.description = description
434 pull_request.description_renderer = description_renderer
434 pull_request.description_renderer = description_renderer
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437 pull_request.pull_request_state = pull_request.STATE_CREATING
437 pull_request.pull_request_state = pull_request.STATE_CREATING
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory, rules = reviewer_object
444 user_id, reasons, mandatory, rules = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458
458
459 # NOTE(marcink): pick only first rule for now
459 # NOTE(marcink): pick only first rule for now
460 rule_id = list(rules)[0] if rules else None
460 rule_id = list(rules)[0] if rules else None
461 rule = RepoReviewRule.get(rule_id) if rule_id else None
461 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 if rule:
462 if rule:
463 review_group = rule.user_group_vote_rule(user_id)
463 review_group = rule.user_group_vote_rule(user_id)
464 # we check if this particular reviewer is member of a voting group
464 # we check if this particular reviewer is member of a voting group
465 if review_group:
465 if review_group:
466 # NOTE(marcink):
466 # NOTE(marcink):
467 # can be that user is member of more but we pick the first same,
467 # can be that user is member of more but we pick the first same,
468 # same as default reviewers algo
468 # same as default reviewers algo
469 review_group = review_group[0]
469 review_group = review_group[0]
470
470
471 rule_data = {
471 rule_data = {
472 'rule_name':
472 'rule_name':
473 rule.review_rule_name,
473 rule.review_rule_name,
474 'rule_user_group_entry_id':
474 'rule_user_group_entry_id':
475 review_group.repo_review_rule_users_group_id,
475 review_group.repo_review_rule_users_group_id,
476 'rule_user_group_name':
476 'rule_user_group_name':
477 review_group.users_group.users_group_name,
477 review_group.users_group.users_group_name,
478 'rule_user_group_members':
478 'rule_user_group_members':
479 [x.user.username for x in review_group.users_group.members],
479 [x.user.username for x in review_group.users_group.members],
480 'rule_user_group_members_id':
480 'rule_user_group_members_id':
481 [x.user.user_id for x in review_group.users_group.members],
481 [x.user.user_id for x in review_group.users_group.members],
482 }
482 }
483 # e.g {'vote_rule': -1, 'mandatory': True}
483 # e.g {'vote_rule': -1, 'mandatory': True}
484 rule_data.update(review_group.rule_data())
484 rule_data.update(review_group.rule_data())
485
485
486 reviewer.rule_data = rule_data
486 reviewer.rule_data = rule_data
487
487
488 Session().add(reviewer)
488 Session().add(reviewer)
489 Session().flush()
489 Session().flush()
490
490
491 # Set approval status to "Under Review" for all commits which are
491 # Set approval status to "Under Review" for all commits which are
492 # part of this pull request.
492 # part of this pull request.
493 ChangesetStatusModel().set_status(
493 ChangesetStatusModel().set_status(
494 repo=target_repo,
494 repo=target_repo,
495 status=ChangesetStatus.STATUS_UNDER_REVIEW,
495 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 user=created_by_user,
496 user=created_by_user,
497 pull_request=pull_request
497 pull_request=pull_request
498 )
498 )
499 # we commit early at this point. This has to do with a fact
499 # we commit early at this point. This has to do with a fact
500 # that before queries do some row-locking. And because of that
500 # that before queries do some row-locking. And because of that
501 # we need to commit and finish transaction before below validate call
501 # we need to commit and finish transaction before below validate call
502 # that for large repos could be long resulting in long row locks
502 # that for large repos could be long resulting in long row locks
503 Session().commit()
503 Session().commit()
504
504
505 # prepare workspace, and run initial merge simulation. Set state during that
505 # prepare workspace, and run initial merge simulation. Set state during that
506 # operation
506 # operation
507 pull_request = PullRequest.get(pull_request.pull_request_id)
507 pull_request = PullRequest.get(pull_request.pull_request_id)
508
508
509 # set as merging, for simulation, and if finished to created so we mark
509 # set as merging, for simulation, and if finished to created so we mark
510 # simulation is working fine
510 # simulation is working fine
511 with pull_request.set_state(PullRequest.STATE_MERGING,
511 with pull_request.set_state(PullRequest.STATE_MERGING,
512 final_state=PullRequest.STATE_CREATED):
512 final_state=PullRequest.STATE_CREATED):
513 MergeCheck.validate(
513 MergeCheck.validate(
514 pull_request, auth_user=auth_user, translator=translator)
514 pull_request, auth_user=auth_user, translator=translator)
515
515
516 self.notify_reviewers(pull_request, reviewer_ids)
516 self.notify_reviewers(pull_request, reviewer_ids)
517 self.trigger_pull_request_hook(
517 self.trigger_pull_request_hook(
518 pull_request, created_by_user, 'create')
518 pull_request, created_by_user, 'create')
519
519
520 creation_data = pull_request.get_api_data(with_merge_state=False)
520 creation_data = pull_request.get_api_data(with_merge_state=False)
521 self._log_audit_action(
521 self._log_audit_action(
522 'repo.pull_request.create', {'data': creation_data},
522 'repo.pull_request.create', {'data': creation_data},
523 auth_user, pull_request)
523 auth_user, pull_request)
524
524
525 return pull_request
525 return pull_request
526
526
527 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
527 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 pull_request = self.__get_pull_request(pull_request)
528 pull_request = self.__get_pull_request(pull_request)
529 target_scm = pull_request.target_repo.scm_instance()
529 target_scm = pull_request.target_repo.scm_instance()
530 if action == 'create':
530 if action == 'create':
531 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
531 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 elif action == 'merge':
532 elif action == 'merge':
533 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
533 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 elif action == 'close':
534 elif action == 'close':
535 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
535 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 elif action == 'review_status_change':
536 elif action == 'review_status_change':
537 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
537 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 elif action == 'update':
538 elif action == 'update':
539 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
539 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 elif action == 'comment':
540 elif action == 'comment':
541 # dummy hook ! for comment. We want this function to handle all cases
541 # dummy hook ! for comment. We want this function to handle all cases
542 def trigger_hook(*args, **kwargs):
542 def trigger_hook(*args, **kwargs):
543 pass
543 pass
544 comment = data['comment']
544 comment = data['comment']
545 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
545 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 else:
546 else:
547 return
547 return
548
548
549 trigger_hook(
549 trigger_hook(
550 username=user.username,
550 username=user.username,
551 repo_name=pull_request.target_repo.repo_name,
551 repo_name=pull_request.target_repo.repo_name,
552 repo_alias=target_scm.alias,
552 repo_alias=target_scm.alias,
553 pull_request=pull_request,
553 pull_request=pull_request,
554 data=data)
554 data=data)
555
555
556 def _get_commit_ids(self, pull_request):
556 def _get_commit_ids(self, pull_request):
557 """
557 """
558 Return the commit ids of the merged pull request.
558 Return the commit ids of the merged pull request.
559
559
560 This method is not dealing correctly yet with the lack of autoupdates
560 This method is not dealing correctly yet with the lack of autoupdates
561 nor with the implicit target updates.
561 nor with the implicit target updates.
562 For example: if a commit in the source repo is already in the target it
562 For example: if a commit in the source repo is already in the target it
563 will be reported anyways.
563 will be reported anyways.
564 """
564 """
565 merge_rev = pull_request.merge_rev
565 merge_rev = pull_request.merge_rev
566 if merge_rev is None:
566 if merge_rev is None:
567 raise ValueError('This pull request was not merged yet')
567 raise ValueError('This pull request was not merged yet')
568
568
569 commit_ids = list(pull_request.revisions)
569 commit_ids = list(pull_request.revisions)
570 if merge_rev not in commit_ids:
570 if merge_rev not in commit_ids:
571 commit_ids.append(merge_rev)
571 commit_ids.append(merge_rev)
572
572
573 return commit_ids
573 return commit_ids
574
574
575 def merge_repo(self, pull_request, user, extras):
575 def merge_repo(self, pull_request, user, extras):
576 log.debug("Merging pull request %s", pull_request.pull_request_id)
576 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 extras['user_agent'] = 'internal-merge'
577 extras['user_agent'] = 'internal-merge'
578 merge_state = self._merge_pull_request(pull_request, user, extras)
578 merge_state = self._merge_pull_request(pull_request, user, extras)
579 if merge_state.executed:
579 if merge_state.executed:
580 log.debug("Merge was successful, updating the pull request comments.")
580 log.debug("Merge was successful, updating the pull request comments.")
581 self._comment_and_close_pr(pull_request, user, merge_state)
581 self._comment_and_close_pr(pull_request, user, merge_state)
582
582
583 self._log_audit_action(
583 self._log_audit_action(
584 'repo.pull_request.merge',
584 'repo.pull_request.merge',
585 {'merge_state': merge_state.__dict__},
585 {'merge_state': merge_state.__dict__},
586 user, pull_request)
586 user, pull_request)
587
587
588 else:
588 else:
589 log.warn("Merge failed, not updating the pull request.")
589 log.warn("Merge failed, not updating the pull request.")
590 return merge_state
590 return merge_state
591
591
592 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
592 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 target_vcs = pull_request.target_repo.scm_instance()
593 target_vcs = pull_request.target_repo.scm_instance()
594 source_vcs = pull_request.source_repo.scm_instance()
594 source_vcs = pull_request.source_repo.scm_instance()
595
595
596 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
596 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 pr_id=pull_request.pull_request_id,
597 pr_id=pull_request.pull_request_id,
598 pr_title=pull_request.title,
598 pr_title=pull_request.title,
599 source_repo=source_vcs.name,
599 source_repo=source_vcs.name,
600 source_ref_name=pull_request.source_ref_parts.name,
600 source_ref_name=pull_request.source_ref_parts.name,
601 target_repo=target_vcs.name,
601 target_repo=target_vcs.name,
602 target_ref_name=pull_request.target_ref_parts.name,
602 target_ref_name=pull_request.target_ref_parts.name,
603 )
603 )
604
604
605 workspace_id = self._workspace_id(pull_request)
605 workspace_id = self._workspace_id(pull_request)
606 repo_id = pull_request.target_repo.repo_id
606 repo_id = pull_request.target_repo.repo_id
607 use_rebase = self._use_rebase_for_merging(pull_request)
607 use_rebase = self._use_rebase_for_merging(pull_request)
608 close_branch = self._close_branch_before_merging(pull_request)
608 close_branch = self._close_branch_before_merging(pull_request)
609
609
610 target_ref = self._refresh_reference(
610 target_ref = self._refresh_reference(
611 pull_request.target_ref_parts, target_vcs)
611 pull_request.target_ref_parts, target_vcs)
612
612
613 callback_daemon, extras = prepare_callback_daemon(
613 callback_daemon, extras = prepare_callback_daemon(
614 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
614 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 host=vcs_settings.HOOKS_HOST,
615 host=vcs_settings.HOOKS_HOST,
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617
617
618 with callback_daemon:
618 with callback_daemon:
619 # TODO: johbo: Implement a clean way to run a config_override
619 # TODO: johbo: Implement a clean way to run a config_override
620 # for a single call.
620 # for a single call.
621 target_vcs.config.set(
621 target_vcs.config.set(
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623
623
624 user_name = user.short_contact
624 user_name = user.short_contact
625 merge_state = target_vcs.merge(
625 merge_state = target_vcs.merge(
626 repo_id, workspace_id, target_ref, source_vcs,
626 repo_id, workspace_id, target_ref, source_vcs,
627 pull_request.source_ref_parts,
627 pull_request.source_ref_parts,
628 user_name=user_name, user_email=user.email,
628 user_name=user_name, user_email=user.email,
629 message=message, use_rebase=use_rebase,
629 message=message, use_rebase=use_rebase,
630 close_branch=close_branch)
630 close_branch=close_branch)
631 return merge_state
631 return merge_state
632
632
633 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
633 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 pull_request.merge_rev = merge_state.merge_ref.commit_id
634 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.updated_on = datetime.datetime.now()
635 pull_request.updated_on = datetime.datetime.now()
636 close_msg = close_msg or 'Pull request merged and closed'
636 close_msg = close_msg or 'Pull request merged and closed'
637
637
638 CommentsModel().create(
638 CommentsModel().create(
639 text=safe_unicode(close_msg),
639 text=safe_unicode(close_msg),
640 repo=pull_request.target_repo.repo_id,
640 repo=pull_request.target_repo.repo_id,
641 user=user.user_id,
641 user=user.user_id,
642 pull_request=pull_request.pull_request_id,
642 pull_request=pull_request.pull_request_id,
643 f_path=None,
643 f_path=None,
644 line_no=None,
644 line_no=None,
645 closing_pr=True
645 closing_pr=True
646 )
646 )
647
647
648 Session().add(pull_request)
648 Session().add(pull_request)
649 Session().flush()
649 Session().flush()
650 # TODO: paris: replace invalidation with less radical solution
650 # TODO: paris: replace invalidation with less radical solution
651 ScmModel().mark_for_invalidation(
651 ScmModel().mark_for_invalidation(
652 pull_request.target_repo.repo_name)
652 pull_request.target_repo.repo_name)
653 self.trigger_pull_request_hook(pull_request, user, 'merge')
653 self.trigger_pull_request_hook(pull_request, user, 'merge')
654
654
655 def has_valid_update_type(self, pull_request):
655 def has_valid_update_type(self, pull_request):
656 source_ref_type = pull_request.source_ref_parts.type
656 source_ref_type = pull_request.source_ref_parts.type
657 return source_ref_type in self.REF_TYPES
657 return source_ref_type in self.REF_TYPES
658
658
659 def update_commits(self, pull_request):
659 def update_commits(self, pull_request):
660 """
660 """
661 Get the updated list of commits for the pull request
661 Get the updated list of commits for the pull request
662 and return the new pull request version and the list
662 and return the new pull request version and the list
663 of commits processed by this update action
663 of commits processed by this update action
664 """
664 """
665 pull_request = self.__get_pull_request(pull_request)
665 pull_request = self.__get_pull_request(pull_request)
666 source_ref_type = pull_request.source_ref_parts.type
666 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_name = pull_request.source_ref_parts.name
667 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_id = pull_request.source_ref_parts.commit_id
668 source_ref_id = pull_request.source_ref_parts.commit_id
669
669
670 target_ref_type = pull_request.target_ref_parts.type
670 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_name = pull_request.target_ref_parts.name
671 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_id = pull_request.target_ref_parts.commit_id
672 target_ref_id = pull_request.target_ref_parts.commit_id
673
673
674 if not self.has_valid_update_type(pull_request):
674 if not self.has_valid_update_type(pull_request):
675 log.debug("Skipping update of pull request %s due to ref type: %s",
675 log.debug("Skipping update of pull request %s due to ref type: %s",
676 pull_request, source_ref_type)
676 pull_request, source_ref_type)
677 return UpdateResponse(
677 return UpdateResponse(
678 executed=False,
678 executed=False,
679 reason=UpdateFailureReason.WRONG_REF_TYPE,
679 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 old=pull_request, new=None, changes=None,
680 old=pull_request, new=None, changes=None,
681 source_changed=False, target_changed=False)
681 source_changed=False, target_changed=False)
682
682
683 # source repo
683 # source repo
684 source_repo = pull_request.source_repo.scm_instance()
684 source_repo = pull_request.source_repo.scm_instance()
685 try:
685 try:
686 source_commit = source_repo.get_commit(commit_id=source_ref_name)
686 source_commit = source_repo.get_commit(commit_id=source_ref_name)
687 except CommitDoesNotExistError:
687 except CommitDoesNotExistError:
688 return UpdateResponse(
688 return UpdateResponse(
689 executed=False,
689 executed=False,
690 reason=UpdateFailureReason.MISSING_SOURCE_REF,
690 reason=UpdateFailureReason.MISSING_SOURCE_REF,
691 old=pull_request, new=None, changes=None,
691 old=pull_request, new=None, changes=None,
692 source_changed=False, target_changed=False)
692 source_changed=False, target_changed=False)
693
693
694 source_changed = source_ref_id != source_commit.raw_id
694 source_changed = source_ref_id != source_commit.raw_id
695
695
696 # target repo
696 # target repo
697 target_repo = pull_request.target_repo.scm_instance()
697 target_repo = pull_request.target_repo.scm_instance()
698 try:
698 try:
699 target_commit = target_repo.get_commit(commit_id=target_ref_name)
699 target_commit = target_repo.get_commit(commit_id=target_ref_name)
700 except CommitDoesNotExistError:
700 except CommitDoesNotExistError:
701 return UpdateResponse(
701 return UpdateResponse(
702 executed=False,
702 executed=False,
703 reason=UpdateFailureReason.MISSING_TARGET_REF,
703 reason=UpdateFailureReason.MISSING_TARGET_REF,
704 old=pull_request, new=None, changes=None,
704 old=pull_request, new=None, changes=None,
705 source_changed=False, target_changed=False)
705 source_changed=False, target_changed=False)
706 target_changed = target_ref_id != target_commit.raw_id
706 target_changed = target_ref_id != target_commit.raw_id
707
707
708 if not (source_changed or target_changed):
708 if not (source_changed or target_changed):
709 log.debug("Nothing changed in pull request %s", pull_request)
709 log.debug("Nothing changed in pull request %s", pull_request)
710 return UpdateResponse(
710 return UpdateResponse(
711 executed=False,
711 executed=False,
712 reason=UpdateFailureReason.NO_CHANGE,
712 reason=UpdateFailureReason.NO_CHANGE,
713 old=pull_request, new=None, changes=None,
713 old=pull_request, new=None, changes=None,
714 source_changed=target_changed, target_changed=source_changed)
714 source_changed=target_changed, target_changed=source_changed)
715
715
716 change_in_found = 'target repo' if target_changed else 'source repo'
716 change_in_found = 'target repo' if target_changed else 'source repo'
717 log.debug('Updating pull request because of change in %s detected',
717 log.debug('Updating pull request because of change in %s detected',
718 change_in_found)
718 change_in_found)
719
719
720 # Finally there is a need for an update, in case of source change
720 # Finally there is a need for an update, in case of source change
721 # we create a new version, else just an update
721 # we create a new version, else just an update
722 if source_changed:
722 if source_changed:
723 pull_request_version = self._create_version_from_snapshot(pull_request)
723 pull_request_version = self._create_version_from_snapshot(pull_request)
724 self._link_comments_to_version(pull_request_version)
724 self._link_comments_to_version(pull_request_version)
725 else:
725 else:
726 try:
726 try:
727 ver = pull_request.versions[-1]
727 ver = pull_request.versions[-1]
728 except IndexError:
728 except IndexError:
729 ver = None
729 ver = None
730
730
731 pull_request.pull_request_version_id = \
731 pull_request.pull_request_version_id = \
732 ver.pull_request_version_id if ver else None
732 ver.pull_request_version_id if ver else None
733 pull_request_version = pull_request
733 pull_request_version = pull_request
734
734
735 try:
735 try:
736 if target_ref_type in self.REF_TYPES:
736 if target_ref_type in self.REF_TYPES:
737 target_commit = target_repo.get_commit(target_ref_name)
737 target_commit = target_repo.get_commit(target_ref_name)
738 else:
738 else:
739 target_commit = target_repo.get_commit(target_ref_id)
739 target_commit = target_repo.get_commit(target_ref_id)
740 except CommitDoesNotExistError:
740 except CommitDoesNotExistError:
741 return UpdateResponse(
741 return UpdateResponse(
742 executed=False,
742 executed=False,
743 reason=UpdateFailureReason.MISSING_TARGET_REF,
743 reason=UpdateFailureReason.MISSING_TARGET_REF,
744 old=pull_request, new=None, changes=None,
744 old=pull_request, new=None, changes=None,
745 source_changed=source_changed, target_changed=target_changed)
745 source_changed=source_changed, target_changed=target_changed)
746
746
747 # re-compute commit ids
747 # re-compute commit ids
748 old_commit_ids = pull_request.revisions
748 old_commit_ids = pull_request.revisions
749 pre_load = ["author", "branch", "date", "message"]
749 pre_load = ["author", "branch", "date", "message"]
750 commit_ranges = target_repo.compare(
750 commit_ranges = target_repo.compare(
751 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
751 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
752 pre_load=pre_load)
752 pre_load=pre_load)
753
753
754 ancestor = target_repo.get_common_ancestor(
754 ancestor = target_repo.get_common_ancestor(
755 target_commit.raw_id, source_commit.raw_id, source_repo)
755 target_commit.raw_id, source_commit.raw_id, source_repo)
756
756
757 pull_request.source_ref = '%s:%s:%s' % (
757 pull_request.source_ref = '%s:%s:%s' % (
758 source_ref_type, source_ref_name, source_commit.raw_id)
758 source_ref_type, source_ref_name, source_commit.raw_id)
759 pull_request.target_ref = '%s:%s:%s' % (
759 pull_request.target_ref = '%s:%s:%s' % (
760 target_ref_type, target_ref_name, ancestor)
760 target_ref_type, target_ref_name, ancestor)
761
761
762 pull_request.revisions = [
762 pull_request.revisions = [
763 commit.raw_id for commit in reversed(commit_ranges)]
763 commit.raw_id for commit in reversed(commit_ranges)]
764 pull_request.updated_on = datetime.datetime.now()
764 pull_request.updated_on = datetime.datetime.now()
765 Session().add(pull_request)
765 Session().add(pull_request)
766 new_commit_ids = pull_request.revisions
766 new_commit_ids = pull_request.revisions
767
767
768 old_diff_data, new_diff_data = self._generate_update_diffs(
768 old_diff_data, new_diff_data = self._generate_update_diffs(
769 pull_request, pull_request_version)
769 pull_request, pull_request_version)
770
770
771 # calculate commit and file changes
771 # calculate commit and file changes
772 changes = self._calculate_commit_id_changes(
772 changes = self._calculate_commit_id_changes(
773 old_commit_ids, new_commit_ids)
773 old_commit_ids, new_commit_ids)
774 file_changes = self._calculate_file_changes(
774 file_changes = self._calculate_file_changes(
775 old_diff_data, new_diff_data)
775 old_diff_data, new_diff_data)
776
776
777 # set comments as outdated if DIFFS changed
777 # set comments as outdated if DIFFS changed
778 CommentsModel().outdate_comments(
778 CommentsModel().outdate_comments(
779 pull_request, old_diff_data=old_diff_data,
779 pull_request, old_diff_data=old_diff_data,
780 new_diff_data=new_diff_data)
780 new_diff_data=new_diff_data)
781
781
782 commit_changes = (changes.added or changes.removed)
782 commit_changes = (changes.added or changes.removed)
783 file_node_changes = (
783 file_node_changes = (
784 file_changes.added or file_changes.modified or file_changes.removed)
784 file_changes.added or file_changes.modified or file_changes.removed)
785 pr_has_changes = commit_changes or file_node_changes
785 pr_has_changes = commit_changes or file_node_changes
786
786
787 # Add an automatic comment to the pull request, in case
787 # Add an automatic comment to the pull request, in case
788 # anything has changed
788 # anything has changed
789 if pr_has_changes:
789 if pr_has_changes:
790 update_comment = CommentsModel().create(
790 update_comment = CommentsModel().create(
791 text=self._render_update_message(changes, file_changes),
791 text=self._render_update_message(changes, file_changes),
792 repo=pull_request.target_repo,
792 repo=pull_request.target_repo,
793 user=pull_request.author,
793 user=pull_request.author,
794 pull_request=pull_request,
794 pull_request=pull_request,
795 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
795 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
796
796
797 # Update status to "Under Review" for added commits
797 # Update status to "Under Review" for added commits
798 for commit_id in changes.added:
798 for commit_id in changes.added:
799 ChangesetStatusModel().set_status(
799 ChangesetStatusModel().set_status(
800 repo=pull_request.source_repo,
800 repo=pull_request.source_repo,
801 status=ChangesetStatus.STATUS_UNDER_REVIEW,
801 status=ChangesetStatus.STATUS_UNDER_REVIEW,
802 comment=update_comment,
802 comment=update_comment,
803 user=pull_request.author,
803 user=pull_request.author,
804 pull_request=pull_request,
804 pull_request=pull_request,
805 revision=commit_id)
805 revision=commit_id)
806
806
807 log.debug(
807 log.debug(
808 'Updated pull request %s, added_ids: %s, common_ids: %s, '
808 'Updated pull request %s, added_ids: %s, common_ids: %s, '
809 'removed_ids: %s', pull_request.pull_request_id,
809 'removed_ids: %s', pull_request.pull_request_id,
810 changes.added, changes.common, changes.removed)
810 changes.added, changes.common, changes.removed)
811 log.debug(
811 log.debug(
812 'Updated pull request with the following file changes: %s',
812 'Updated pull request with the following file changes: %s',
813 file_changes)
813 file_changes)
814
814
815 log.info(
815 log.info(
816 "Updated pull request %s from commit %s to commit %s, "
816 "Updated pull request %s from commit %s to commit %s, "
817 "stored new version %s of this pull request.",
817 "stored new version %s of this pull request.",
818 pull_request.pull_request_id, source_ref_id,
818 pull_request.pull_request_id, source_ref_id,
819 pull_request.source_ref_parts.commit_id,
819 pull_request.source_ref_parts.commit_id,
820 pull_request_version.pull_request_version_id)
820 pull_request_version.pull_request_version_id)
821 Session().commit()
821 Session().commit()
822 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
822 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
823
823
824 return UpdateResponse(
824 return UpdateResponse(
825 executed=True, reason=UpdateFailureReason.NONE,
825 executed=True, reason=UpdateFailureReason.NONE,
826 old=pull_request, new=pull_request_version, changes=changes,
826 old=pull_request, new=pull_request_version, changes=changes,
827 source_changed=source_changed, target_changed=target_changed)
827 source_changed=source_changed, target_changed=target_changed)
828
828
829 def _create_version_from_snapshot(self, pull_request):
829 def _create_version_from_snapshot(self, pull_request):
830 version = PullRequestVersion()
830 version = PullRequestVersion()
831 version.title = pull_request.title
831 version.title = pull_request.title
832 version.description = pull_request.description
832 version.description = pull_request.description
833 version.status = pull_request.status
833 version.status = pull_request.status
834 version.pull_request_state = pull_request.pull_request_state
834 version.pull_request_state = pull_request.pull_request_state
835 version.created_on = datetime.datetime.now()
835 version.created_on = datetime.datetime.now()
836 version.updated_on = pull_request.updated_on
836 version.updated_on = pull_request.updated_on
837 version.user_id = pull_request.user_id
837 version.user_id = pull_request.user_id
838 version.source_repo = pull_request.source_repo
838 version.source_repo = pull_request.source_repo
839 version.source_ref = pull_request.source_ref
839 version.source_ref = pull_request.source_ref
840 version.target_repo = pull_request.target_repo
840 version.target_repo = pull_request.target_repo
841 version.target_ref = pull_request.target_ref
841 version.target_ref = pull_request.target_ref
842
842
843 version._last_merge_source_rev = pull_request._last_merge_source_rev
843 version._last_merge_source_rev = pull_request._last_merge_source_rev
844 version._last_merge_target_rev = pull_request._last_merge_target_rev
844 version._last_merge_target_rev = pull_request._last_merge_target_rev
845 version.last_merge_status = pull_request.last_merge_status
845 version.last_merge_status = pull_request.last_merge_status
846 version.shadow_merge_ref = pull_request.shadow_merge_ref
846 version.shadow_merge_ref = pull_request.shadow_merge_ref
847 version.merge_rev = pull_request.merge_rev
847 version.merge_rev = pull_request.merge_rev
848 version.reviewer_data = pull_request.reviewer_data
848 version.reviewer_data = pull_request.reviewer_data
849
849
850 version.revisions = pull_request.revisions
850 version.revisions = pull_request.revisions
851 version.pull_request = pull_request
851 version.pull_request = pull_request
852 Session().add(version)
852 Session().add(version)
853 Session().flush()
853 Session().flush()
854
854
855 return version
855 return version
856
856
857 def _generate_update_diffs(self, pull_request, pull_request_version):
857 def _generate_update_diffs(self, pull_request, pull_request_version):
858
858
859 diff_context = (
859 diff_context = (
860 self.DIFF_CONTEXT +
860 self.DIFF_CONTEXT +
861 CommentsModel.needed_extra_diff_context())
861 CommentsModel.needed_extra_diff_context())
862 hide_whitespace_changes = False
862 hide_whitespace_changes = False
863 source_repo = pull_request_version.source_repo
863 source_repo = pull_request_version.source_repo
864 source_ref_id = pull_request_version.source_ref_parts.commit_id
864 source_ref_id = pull_request_version.source_ref_parts.commit_id
865 target_ref_id = pull_request_version.target_ref_parts.commit_id
865 target_ref_id = pull_request_version.target_ref_parts.commit_id
866 old_diff = self._get_diff_from_pr_or_version(
866 old_diff = self._get_diff_from_pr_or_version(
867 source_repo, source_ref_id, target_ref_id,
867 source_repo, source_ref_id, target_ref_id,
868 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
868 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
869
869
870 source_repo = pull_request.source_repo
870 source_repo = pull_request.source_repo
871 source_ref_id = pull_request.source_ref_parts.commit_id
871 source_ref_id = pull_request.source_ref_parts.commit_id
872 target_ref_id = pull_request.target_ref_parts.commit_id
872 target_ref_id = pull_request.target_ref_parts.commit_id
873
873
874 new_diff = self._get_diff_from_pr_or_version(
874 new_diff = self._get_diff_from_pr_or_version(
875 source_repo, source_ref_id, target_ref_id,
875 source_repo, source_ref_id, target_ref_id,
876 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
876 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
877
877
878 old_diff_data = diffs.DiffProcessor(old_diff)
878 old_diff_data = diffs.DiffProcessor(old_diff)
879 old_diff_data.prepare()
879 old_diff_data.prepare()
880 new_diff_data = diffs.DiffProcessor(new_diff)
880 new_diff_data = diffs.DiffProcessor(new_diff)
881 new_diff_data.prepare()
881 new_diff_data.prepare()
882
882
883 return old_diff_data, new_diff_data
883 return old_diff_data, new_diff_data
884
884
885 def _link_comments_to_version(self, pull_request_version):
885 def _link_comments_to_version(self, pull_request_version):
886 """
886 """
887 Link all unlinked comments of this pull request to the given version.
887 Link all unlinked comments of this pull request to the given version.
888
888
889 :param pull_request_version: The `PullRequestVersion` to which
889 :param pull_request_version: The `PullRequestVersion` to which
890 the comments shall be linked.
890 the comments shall be linked.
891
891
892 """
892 """
893 pull_request = pull_request_version.pull_request
893 pull_request = pull_request_version.pull_request
894 comments = ChangesetComment.query()\
894 comments = ChangesetComment.query()\
895 .filter(
895 .filter(
896 # TODO: johbo: Should we query for the repo at all here?
896 # TODO: johbo: Should we query for the repo at all here?
897 # Pending decision on how comments of PRs are to be related
897 # Pending decision on how comments of PRs are to be related
898 # to either the source repo, the target repo or no repo at all.
898 # to either the source repo, the target repo or no repo at all.
899 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
899 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
900 ChangesetComment.pull_request == pull_request,
900 ChangesetComment.pull_request == pull_request,
901 ChangesetComment.pull_request_version == None)\
901 ChangesetComment.pull_request_version == None)\
902 .order_by(ChangesetComment.comment_id.asc())
902 .order_by(ChangesetComment.comment_id.asc())
903
903
904 # TODO: johbo: Find out why this breaks if it is done in a bulk
904 # TODO: johbo: Find out why this breaks if it is done in a bulk
905 # operation.
905 # operation.
906 for comment in comments:
906 for comment in comments:
907 comment.pull_request_version_id = (
907 comment.pull_request_version_id = (
908 pull_request_version.pull_request_version_id)
908 pull_request_version.pull_request_version_id)
909 Session().add(comment)
909 Session().add(comment)
910
910
911 def _calculate_commit_id_changes(self, old_ids, new_ids):
911 def _calculate_commit_id_changes(self, old_ids, new_ids):
912 added = [x for x in new_ids if x not in old_ids]
912 added = [x for x in new_ids if x not in old_ids]
913 common = [x for x in new_ids if x in old_ids]
913 common = [x for x in new_ids if x in old_ids]
914 removed = [x for x in old_ids if x not in new_ids]
914 removed = [x for x in old_ids if x not in new_ids]
915 total = new_ids
915 total = new_ids
916 return ChangeTuple(added, common, removed, total)
916 return ChangeTuple(added, common, removed, total)
917
917
918 def _calculate_file_changes(self, old_diff_data, new_diff_data):
918 def _calculate_file_changes(self, old_diff_data, new_diff_data):
919
919
920 old_files = OrderedDict()
920 old_files = OrderedDict()
921 for diff_data in old_diff_data.parsed_diff:
921 for diff_data in old_diff_data.parsed_diff:
922 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
922 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
923
923
924 added_files = []
924 added_files = []
925 modified_files = []
925 modified_files = []
926 removed_files = []
926 removed_files = []
927 for diff_data in new_diff_data.parsed_diff:
927 for diff_data in new_diff_data.parsed_diff:
928 new_filename = diff_data['filename']
928 new_filename = diff_data['filename']
929 new_hash = md5_safe(diff_data['raw_diff'])
929 new_hash = md5_safe(diff_data['raw_diff'])
930
930
931 old_hash = old_files.get(new_filename)
931 old_hash = old_files.get(new_filename)
932 if not old_hash:
932 if not old_hash:
933 # file is not present in old diff, means it's added
933 # file is not present in old diff, means it's added
934 added_files.append(new_filename)
934 added_files.append(new_filename)
935 else:
935 else:
936 if new_hash != old_hash:
936 if new_hash != old_hash:
937 modified_files.append(new_filename)
937 modified_files.append(new_filename)
938 # now remove a file from old, since we have seen it already
938 # now remove a file from old, since we have seen it already
939 del old_files[new_filename]
939 del old_files[new_filename]
940
940
941 # removed files is when there are present in old, but not in NEW,
941 # removed files is when there are present in old, but not in NEW,
942 # since we remove old files that are present in new diff, left-overs
942 # since we remove old files that are present in new diff, left-overs
943 # if any should be the removed files
943 # if any should be the removed files
944 removed_files.extend(old_files.keys())
944 removed_files.extend(old_files.keys())
945
945
946 return FileChangeTuple(added_files, modified_files, removed_files)
946 return FileChangeTuple(added_files, modified_files, removed_files)
947
947
948 def _render_update_message(self, changes, file_changes):
948 def _render_update_message(self, changes, file_changes):
949 """
949 """
950 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
950 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
951 so it's always looking the same disregarding on which default
951 so it's always looking the same disregarding on which default
952 renderer system is using.
952 renderer system is using.
953
953
954 :param changes: changes named tuple
954 :param changes: changes named tuple
955 :param file_changes: file changes named tuple
955 :param file_changes: file changes named tuple
956
956
957 """
957 """
958 new_status = ChangesetStatus.get_status_lbl(
958 new_status = ChangesetStatus.get_status_lbl(
959 ChangesetStatus.STATUS_UNDER_REVIEW)
959 ChangesetStatus.STATUS_UNDER_REVIEW)
960
960
961 changed_files = (
961 changed_files = (
962 file_changes.added + file_changes.modified + file_changes.removed)
962 file_changes.added + file_changes.modified + file_changes.removed)
963
963
964 params = {
964 params = {
965 'under_review_label': new_status,
965 'under_review_label': new_status,
966 'added_commits': changes.added,
966 'added_commits': changes.added,
967 'removed_commits': changes.removed,
967 'removed_commits': changes.removed,
968 'changed_files': changed_files,
968 'changed_files': changed_files,
969 'added_files': file_changes.added,
969 'added_files': file_changes.added,
970 'modified_files': file_changes.modified,
970 'modified_files': file_changes.modified,
971 'removed_files': file_changes.removed,
971 'removed_files': file_changes.removed,
972 }
972 }
973 renderer = RstTemplateRenderer()
973 renderer = RstTemplateRenderer()
974 return renderer.render('pull_request_update.mako', **params)
974 return renderer.render('pull_request_update.mako', **params)
975
975
976 def edit(self, pull_request, title, description, description_renderer, user):
976 def edit(self, pull_request, title, description, description_renderer, user):
977 pull_request = self.__get_pull_request(pull_request)
977 pull_request = self.__get_pull_request(pull_request)
978 old_data = pull_request.get_api_data(with_merge_state=False)
978 old_data = pull_request.get_api_data(with_merge_state=False)
979 if pull_request.is_closed():
979 if pull_request.is_closed():
980 raise ValueError('This pull request is closed')
980 raise ValueError('This pull request is closed')
981 if title:
981 if title:
982 pull_request.title = title
982 pull_request.title = title
983 pull_request.description = description
983 pull_request.description = description
984 pull_request.updated_on = datetime.datetime.now()
984 pull_request.updated_on = datetime.datetime.now()
985 pull_request.description_renderer = description_renderer
985 pull_request.description_renderer = description_renderer
986 Session().add(pull_request)
986 Session().add(pull_request)
987 self._log_audit_action(
987 self._log_audit_action(
988 'repo.pull_request.edit', {'old_data': old_data},
988 'repo.pull_request.edit', {'old_data': old_data},
989 user, pull_request)
989 user, pull_request)
990
990
991 def update_reviewers(self, pull_request, reviewer_data, user):
991 def update_reviewers(self, pull_request, reviewer_data, user):
992 """
992 """
993 Update the reviewers in the pull request
993 Update the reviewers in the pull request
994
994
995 :param pull_request: the pr to update
995 :param pull_request: the pr to update
996 :param reviewer_data: list of tuples
996 :param reviewer_data: list of tuples
997 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
997 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
998 """
998 """
999 pull_request = self.__get_pull_request(pull_request)
999 pull_request = self.__get_pull_request(pull_request)
1000 if pull_request.is_closed():
1000 if pull_request.is_closed():
1001 raise ValueError('This pull request is closed')
1001 raise ValueError('This pull request is closed')
1002
1002
1003 reviewers = {}
1003 reviewers = {}
1004 for user_id, reasons, mandatory, rules in reviewer_data:
1004 for user_id, reasons, mandatory, rules in reviewer_data:
1005 if isinstance(user_id, (int, compat.string_types)):
1005 if isinstance(user_id, (int, compat.string_types)):
1006 user_id = self._get_user(user_id).user_id
1006 user_id = self._get_user(user_id).user_id
1007 reviewers[user_id] = {
1007 reviewers[user_id] = {
1008 'reasons': reasons, 'mandatory': mandatory}
1008 'reasons': reasons, 'mandatory': mandatory}
1009
1009
1010 reviewers_ids = set(reviewers.keys())
1010 reviewers_ids = set(reviewers.keys())
1011 current_reviewers = PullRequestReviewers.query()\
1011 current_reviewers = PullRequestReviewers.query()\
1012 .filter(PullRequestReviewers.pull_request ==
1012 .filter(PullRequestReviewers.pull_request ==
1013 pull_request).all()
1013 pull_request).all()
1014 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1014 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1015
1015
1016 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1016 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1017 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1017 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1018
1018
1019 log.debug("Adding %s reviewers", ids_to_add)
1019 log.debug("Adding %s reviewers", ids_to_add)
1020 log.debug("Removing %s reviewers", ids_to_remove)
1020 log.debug("Removing %s reviewers", ids_to_remove)
1021 changed = False
1021 changed = False
1022 for uid in ids_to_add:
1022 for uid in ids_to_add:
1023 changed = True
1023 changed = True
1024 _usr = self._get_user(uid)
1024 _usr = self._get_user(uid)
1025 reviewer = PullRequestReviewers()
1025 reviewer = PullRequestReviewers()
1026 reviewer.user = _usr
1026 reviewer.user = _usr
1027 reviewer.pull_request = pull_request
1027 reviewer.pull_request = pull_request
1028 reviewer.reasons = reviewers[uid]['reasons']
1028 reviewer.reasons = reviewers[uid]['reasons']
1029 # NOTE(marcink): mandatory shouldn't be changed now
1029 # NOTE(marcink): mandatory shouldn't be changed now
1030 # reviewer.mandatory = reviewers[uid]['reasons']
1030 # reviewer.mandatory = reviewers[uid]['reasons']
1031 Session().add(reviewer)
1031 Session().add(reviewer)
1032 self._log_audit_action(
1032 self._log_audit_action(
1033 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1033 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1034 user, pull_request)
1034 user, pull_request)
1035
1035
1036 for uid in ids_to_remove:
1036 for uid in ids_to_remove:
1037 changed = True
1037 changed = True
1038 reviewers = PullRequestReviewers.query()\
1038 reviewers = PullRequestReviewers.query()\
1039 .filter(PullRequestReviewers.user_id == uid,
1039 .filter(PullRequestReviewers.user_id == uid,
1040 PullRequestReviewers.pull_request == pull_request)\
1040 PullRequestReviewers.pull_request == pull_request)\
1041 .all()
1041 .all()
1042 # use .all() in case we accidentally added the same person twice
1042 # use .all() in case we accidentally added the same person twice
1043 # this CAN happen due to the lack of DB checks
1043 # this CAN happen due to the lack of DB checks
1044 for obj in reviewers:
1044 for obj in reviewers:
1045 old_data = obj.get_dict()
1045 old_data = obj.get_dict()
1046 Session().delete(obj)
1046 Session().delete(obj)
1047 self._log_audit_action(
1047 self._log_audit_action(
1048 'repo.pull_request.reviewer.delete',
1048 'repo.pull_request.reviewer.delete',
1049 {'old_data': old_data}, user, pull_request)
1049 {'old_data': old_data}, user, pull_request)
1050
1050
1051 if changed:
1051 if changed:
1052 pull_request.updated_on = datetime.datetime.now()
1052 pull_request.updated_on = datetime.datetime.now()
1053 Session().add(pull_request)
1053 Session().add(pull_request)
1054
1054
1055 self.notify_reviewers(pull_request, ids_to_add)
1055 self.notify_reviewers(pull_request, ids_to_add)
1056 return ids_to_add, ids_to_remove
1056 return ids_to_add, ids_to_remove
1057
1057
1058 def get_url(self, pull_request, request=None, permalink=False):
1058 def get_url(self, pull_request, request=None, permalink=False):
1059 if not request:
1059 if not request:
1060 request = get_current_request()
1060 request = get_current_request()
1061
1061
1062 if permalink:
1062 if permalink:
1063 return request.route_url(
1063 return request.route_url(
1064 'pull_requests_global',
1064 'pull_requests_global',
1065 pull_request_id=pull_request.pull_request_id,)
1065 pull_request_id=pull_request.pull_request_id,)
1066 else:
1066 else:
1067 return request.route_url('pullrequest_show',
1067 return request.route_url('pullrequest_show',
1068 repo_name=safe_str(pull_request.target_repo.repo_name),
1068 repo_name=safe_str(pull_request.target_repo.repo_name),
1069 pull_request_id=pull_request.pull_request_id,)
1069 pull_request_id=pull_request.pull_request_id,)
1070
1070
1071 def get_shadow_clone_url(self, pull_request, request=None):
1071 def get_shadow_clone_url(self, pull_request, request=None):
1072 """
1072 """
1073 Returns qualified url pointing to the shadow repository. If this pull
1073 Returns qualified url pointing to the shadow repository. If this pull
1074 request is closed there is no shadow repository and ``None`` will be
1074 request is closed there is no shadow repository and ``None`` will be
1075 returned.
1075 returned.
1076 """
1076 """
1077 if pull_request.is_closed():
1077 if pull_request.is_closed():
1078 return None
1078 return None
1079 else:
1079 else:
1080 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1080 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1081 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1081 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1082
1082
1083 def notify_reviewers(self, pull_request, reviewers_ids):
1083 def notify_reviewers(self, pull_request, reviewers_ids):
1084 # notification to reviewers
1084 # notification to reviewers
1085 if not reviewers_ids:
1085 if not reviewers_ids:
1086 return
1086 return
1087
1087
1088 pull_request_obj = pull_request
1088 pull_request_obj = pull_request
1089 # get the current participants of this pull request
1089 # get the current participants of this pull request
1090 recipients = reviewers_ids
1090 recipients = reviewers_ids
1091 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1091 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1092
1092
1093 pr_source_repo = pull_request_obj.source_repo
1093 pr_source_repo = pull_request_obj.source_repo
1094 pr_target_repo = pull_request_obj.target_repo
1094 pr_target_repo = pull_request_obj.target_repo
1095
1095
1096 pr_url = h.route_url('pullrequest_show',
1096 pr_url = h.route_url('pullrequest_show',
1097 repo_name=pr_target_repo.repo_name,
1097 repo_name=pr_target_repo.repo_name,
1098 pull_request_id=pull_request_obj.pull_request_id,)
1098 pull_request_id=pull_request_obj.pull_request_id,)
1099
1099
1100 # set some variables for email notification
1100 # set some variables for email notification
1101 pr_target_repo_url = h.route_url(
1101 pr_target_repo_url = h.route_url(
1102 'repo_summary', repo_name=pr_target_repo.repo_name)
1102 'repo_summary', repo_name=pr_target_repo.repo_name)
1103
1103
1104 pr_source_repo_url = h.route_url(
1104 pr_source_repo_url = h.route_url(
1105 'repo_summary', repo_name=pr_source_repo.repo_name)
1105 'repo_summary', repo_name=pr_source_repo.repo_name)
1106
1106
1107 # pull request specifics
1107 # pull request specifics
1108 pull_request_commits = [
1108 pull_request_commits = [
1109 (x.raw_id, x.message)
1109 (x.raw_id, x.message)
1110 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1110 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1111
1111
1112 kwargs = {
1112 kwargs = {
1113 'user': pull_request.author,
1113 'user': pull_request.author,
1114 'pull_request': pull_request_obj,
1114 'pull_request': pull_request_obj,
1115 'pull_request_commits': pull_request_commits,
1115 'pull_request_commits': pull_request_commits,
1116
1116
1117 'pull_request_target_repo': pr_target_repo,
1117 'pull_request_target_repo': pr_target_repo,
1118 'pull_request_target_repo_url': pr_target_repo_url,
1118 'pull_request_target_repo_url': pr_target_repo_url,
1119
1119
1120 'pull_request_source_repo': pr_source_repo,
1120 'pull_request_source_repo': pr_source_repo,
1121 'pull_request_source_repo_url': pr_source_repo_url,
1121 'pull_request_source_repo_url': pr_source_repo_url,
1122
1122
1123 'pull_request_url': pr_url,
1123 'pull_request_url': pr_url,
1124 }
1124 }
1125
1125
1126 # pre-generate the subject for notification itself
1126 # pre-generate the subject for notification itself
1127 (subject,
1127 (subject,
1128 _h, _e, # we don't care about those
1128 _h, _e, # we don't care about those
1129 body_plaintext) = EmailNotificationModel().render_email(
1129 body_plaintext) = EmailNotificationModel().render_email(
1130 notification_type, **kwargs)
1130 notification_type, **kwargs)
1131
1131
1132 # create notification objects, and emails
1132 # create notification objects, and emails
1133 NotificationModel().create(
1133 NotificationModel().create(
1134 created_by=pull_request.author,
1134 created_by=pull_request.author,
1135 notification_subject=subject,
1135 notification_subject=subject,
1136 notification_body=body_plaintext,
1136 notification_body=body_plaintext,
1137 notification_type=notification_type,
1137 notification_type=notification_type,
1138 recipients=recipients,
1138 recipients=recipients,
1139 email_kwargs=kwargs,
1139 email_kwargs=kwargs,
1140 )
1140 )
1141
1141
1142 def delete(self, pull_request, user):
1142 def delete(self, pull_request, user):
1143 pull_request = self.__get_pull_request(pull_request)
1143 pull_request = self.__get_pull_request(pull_request)
1144 old_data = pull_request.get_api_data(with_merge_state=False)
1144 old_data = pull_request.get_api_data(with_merge_state=False)
1145 self._cleanup_merge_workspace(pull_request)
1145 self._cleanup_merge_workspace(pull_request)
1146 self._log_audit_action(
1146 self._log_audit_action(
1147 'repo.pull_request.delete', {'old_data': old_data},
1147 'repo.pull_request.delete', {'old_data': old_data},
1148 user, pull_request)
1148 user, pull_request)
1149 Session().delete(pull_request)
1149 Session().delete(pull_request)
1150
1150
1151 def close_pull_request(self, pull_request, user):
1151 def close_pull_request(self, pull_request, user):
1152 pull_request = self.__get_pull_request(pull_request)
1152 pull_request = self.__get_pull_request(pull_request)
1153 self._cleanup_merge_workspace(pull_request)
1153 self._cleanup_merge_workspace(pull_request)
1154 pull_request.status = PullRequest.STATUS_CLOSED
1154 pull_request.status = PullRequest.STATUS_CLOSED
1155 pull_request.updated_on = datetime.datetime.now()
1155 pull_request.updated_on = datetime.datetime.now()
1156 Session().add(pull_request)
1156 Session().add(pull_request)
1157 self.trigger_pull_request_hook(
1157 self.trigger_pull_request_hook(
1158 pull_request, pull_request.author, 'close')
1158 pull_request, pull_request.author, 'close')
1159
1159
1160 pr_data = pull_request.get_api_data(with_merge_state=False)
1160 pr_data = pull_request.get_api_data(with_merge_state=False)
1161 self._log_audit_action(
1161 self._log_audit_action(
1162 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1162 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1163
1163
1164 def close_pull_request_with_comment(
1164 def close_pull_request_with_comment(
1165 self, pull_request, user, repo, message=None, auth_user=None):
1165 self, pull_request, user, repo, message=None, auth_user=None):
1166
1166
1167 pull_request_review_status = pull_request.calculated_review_status()
1167 pull_request_review_status = pull_request.calculated_review_status()
1168
1168
1169 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1169 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1170 # approved only if we have voting consent
1170 # approved only if we have voting consent
1171 status = ChangesetStatus.STATUS_APPROVED
1171 status = ChangesetStatus.STATUS_APPROVED
1172 else:
1172 else:
1173 status = ChangesetStatus.STATUS_REJECTED
1173 status = ChangesetStatus.STATUS_REJECTED
1174 status_lbl = ChangesetStatus.get_status_lbl(status)
1174 status_lbl = ChangesetStatus.get_status_lbl(status)
1175
1175
1176 default_message = (
1176 default_message = (
1177 'Closing with status change {transition_icon} {status}.'
1177 'Closing with status change {transition_icon} {status}.'
1178 ).format(transition_icon='>', status=status_lbl)
1178 ).format(transition_icon='>', status=status_lbl)
1179 text = message or default_message
1179 text = message or default_message
1180
1180
1181 # create a comment, and link it to new status
1181 # create a comment, and link it to new status
1182 comment = CommentsModel().create(
1182 comment = CommentsModel().create(
1183 text=text,
1183 text=text,
1184 repo=repo.repo_id,
1184 repo=repo.repo_id,
1185 user=user.user_id,
1185 user=user.user_id,
1186 pull_request=pull_request.pull_request_id,
1186 pull_request=pull_request.pull_request_id,
1187 status_change=status_lbl,
1187 status_change=status_lbl,
1188 status_change_type=status,
1188 status_change_type=status,
1189 closing_pr=True,
1189 closing_pr=True,
1190 auth_user=auth_user,
1190 auth_user=auth_user,
1191 )
1191 )
1192
1192
1193 # calculate old status before we change it
1193 # calculate old status before we change it
1194 old_calculated_status = pull_request.calculated_review_status()
1194 old_calculated_status = pull_request.calculated_review_status()
1195 ChangesetStatusModel().set_status(
1195 ChangesetStatusModel().set_status(
1196 repo.repo_id,
1196 repo.repo_id,
1197 status,
1197 status,
1198 user.user_id,
1198 user.user_id,
1199 comment=comment,
1199 comment=comment,
1200 pull_request=pull_request.pull_request_id
1200 pull_request=pull_request.pull_request_id
1201 )
1201 )
1202
1202
1203 Session().flush()
1203 Session().flush()
1204 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1204 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1205 # we now calculate the status of pull request again, and based on that
1205 # we now calculate the status of pull request again, and based on that
1206 # calculation trigger status change. This might happen in cases
1206 # calculation trigger status change. This might happen in cases
1207 # that non-reviewer admin closes a pr, which means his vote doesn't
1207 # that non-reviewer admin closes a pr, which means his vote doesn't
1208 # change the status, while if he's a reviewer this might change it.
1208 # change the status, while if he's a reviewer this might change it.
1209 calculated_status = pull_request.calculated_review_status()
1209 calculated_status = pull_request.calculated_review_status()
1210 if old_calculated_status != calculated_status:
1210 if old_calculated_status != calculated_status:
1211 self.trigger_pull_request_hook(
1211 self.trigger_pull_request_hook(
1212 pull_request, user, 'review_status_change',
1212 pull_request, user, 'review_status_change',
1213 data={'status': calculated_status})
1213 data={'status': calculated_status})
1214
1214
1215 # finally close the PR
1215 # finally close the PR
1216 PullRequestModel().close_pull_request(
1216 PullRequestModel().close_pull_request(
1217 pull_request.pull_request_id, user)
1217 pull_request.pull_request_id, user)
1218
1218
1219 return comment, status
1219 return comment, status
1220
1220
1221 def merge_status(self, pull_request, translator=None,
1221 def merge_status(self, pull_request, translator=None,
1222 force_shadow_repo_refresh=False):
1222 force_shadow_repo_refresh=False):
1223 _ = translator or get_current_request().translate
1223 _ = translator or get_current_request().translate
1224
1224
1225 if not self._is_merge_enabled(pull_request):
1225 if not self._is_merge_enabled(pull_request):
1226 return False, _('Server-side pull request merging is disabled.')
1226 return False, _('Server-side pull request merging is disabled.')
1227 if pull_request.is_closed():
1227 if pull_request.is_closed():
1228 return False, _('This pull request is closed.')
1228 return False, _('This pull request is closed.')
1229 merge_possible, msg = self._check_repo_requirements(
1229 merge_possible, msg = self._check_repo_requirements(
1230 target=pull_request.target_repo, source=pull_request.source_repo,
1230 target=pull_request.target_repo, source=pull_request.source_repo,
1231 translator=_)
1231 translator=_)
1232 if not merge_possible:
1232 if not merge_possible:
1233 return merge_possible, msg
1233 return merge_possible, msg
1234
1234
1235 try:
1235 try:
1236 resp = self._try_merge(
1236 resp = self._try_merge(
1237 pull_request,
1237 pull_request,
1238 force_shadow_repo_refresh=force_shadow_repo_refresh)
1238 force_shadow_repo_refresh=force_shadow_repo_refresh)
1239 log.debug("Merge response: %s", resp)
1239 log.debug("Merge response: %s", resp)
1240 status = resp.possible, resp.merge_status_message
1240 status = resp.possible, resp.merge_status_message
1241 except NotImplementedError:
1241 except NotImplementedError:
1242 status = False, _('Pull request merging is not supported.')
1242 status = False, _('Pull request merging is not supported.')
1243
1243
1244 return status
1244 return status
1245
1245
1246 def _check_repo_requirements(self, target, source, translator):
1246 def _check_repo_requirements(self, target, source, translator):
1247 """
1247 """
1248 Check if `target` and `source` have compatible requirements.
1248 Check if `target` and `source` have compatible requirements.
1249
1249
1250 Currently this is just checking for largefiles.
1250 Currently this is just checking for largefiles.
1251 """
1251 """
1252 _ = translator
1252 _ = translator
1253 target_has_largefiles = self._has_largefiles(target)
1253 target_has_largefiles = self._has_largefiles(target)
1254 source_has_largefiles = self._has_largefiles(source)
1254 source_has_largefiles = self._has_largefiles(source)
1255 merge_possible = True
1255 merge_possible = True
1256 message = u''
1256 message = u''
1257
1257
1258 if target_has_largefiles != source_has_largefiles:
1258 if target_has_largefiles != source_has_largefiles:
1259 merge_possible = False
1259 merge_possible = False
1260 if source_has_largefiles:
1260 if source_has_largefiles:
1261 message = _(
1261 message = _(
1262 'Target repository large files support is disabled.')
1262 'Target repository large files support is disabled.')
1263 else:
1263 else:
1264 message = _(
1264 message = _(
1265 'Source repository large files support is disabled.')
1265 'Source repository large files support is disabled.')
1266
1266
1267 return merge_possible, message
1267 return merge_possible, message
1268
1268
1269 def _has_largefiles(self, repo):
1269 def _has_largefiles(self, repo):
1270 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1270 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1271 'extensions', 'largefiles')
1271 'extensions', 'largefiles')
1272 return largefiles_ui and largefiles_ui[0].active
1272 return largefiles_ui and largefiles_ui[0].active
1273
1273
1274 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1274 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1275 """
1275 """
1276 Try to merge the pull request and return the merge status.
1276 Try to merge the pull request and return the merge status.
1277 """
1277 """
1278 log.debug(
1278 log.debug(
1279 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1279 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1280 pull_request.pull_request_id, force_shadow_repo_refresh)
1280 pull_request.pull_request_id, force_shadow_repo_refresh)
1281 target_vcs = pull_request.target_repo.scm_instance()
1281 target_vcs = pull_request.target_repo.scm_instance()
1282 # Refresh the target reference.
1282 # Refresh the target reference.
1283 try:
1283 try:
1284 target_ref = self._refresh_reference(
1284 target_ref = self._refresh_reference(
1285 pull_request.target_ref_parts, target_vcs)
1285 pull_request.target_ref_parts, target_vcs)
1286 except CommitDoesNotExistError:
1286 except CommitDoesNotExistError:
1287 merge_state = MergeResponse(
1287 merge_state = MergeResponse(
1288 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1288 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1289 metadata={'target_ref': pull_request.target_ref_parts})
1289 metadata={'target_ref': pull_request.target_ref_parts})
1290 return merge_state
1290 return merge_state
1291
1291
1292 target_locked = pull_request.target_repo.locked
1292 target_locked = pull_request.target_repo.locked
1293 if target_locked and target_locked[0]:
1293 if target_locked and target_locked[0]:
1294 locked_by = 'user:{}'.format(target_locked[0])
1294 locked_by = 'user:{}'.format(target_locked[0])
1295 log.debug("The target repository is locked by %s.", locked_by)
1295 log.debug("The target repository is locked by %s.", locked_by)
1296 merge_state = MergeResponse(
1296 merge_state = MergeResponse(
1297 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1297 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1298 metadata={'locked_by': locked_by})
1298 metadata={'locked_by': locked_by})
1299 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1299 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1300 pull_request, target_ref):
1300 pull_request, target_ref):
1301 log.debug("Refreshing the merge status of the repository.")
1301 log.debug("Refreshing the merge status of the repository.")
1302 merge_state = self._refresh_merge_state(
1302 merge_state = self._refresh_merge_state(
1303 pull_request, target_vcs, target_ref)
1303 pull_request, target_vcs, target_ref)
1304 else:
1304 else:
1305 possible = pull_request.\
1305 possible = pull_request.\
1306 last_merge_status == MergeFailureReason.NONE
1306 last_merge_status == MergeFailureReason.NONE
1307 merge_state = MergeResponse(
1307 merge_state = MergeResponse(
1308 possible, False, None, pull_request.last_merge_status)
1308 possible, False, None, pull_request.last_merge_status)
1309
1309
1310 return merge_state
1310 return merge_state
1311
1311
1312 def _refresh_reference(self, reference, vcs_repository):
1312 def _refresh_reference(self, reference, vcs_repository):
1313 if reference.type in self.UPDATABLE_REF_TYPES:
1313 if reference.type in self.UPDATABLE_REF_TYPES:
1314 name_or_id = reference.name
1314 name_or_id = reference.name
1315 else:
1315 else:
1316 name_or_id = reference.commit_id
1316 name_or_id = reference.commit_id
1317 refreshed_commit = vcs_repository.get_commit(name_or_id)
1317 refreshed_commit = vcs_repository.get_commit(name_or_id)
1318 refreshed_reference = Reference(
1318 refreshed_reference = Reference(
1319 reference.type, reference.name, refreshed_commit.raw_id)
1319 reference.type, reference.name, refreshed_commit.raw_id)
1320 return refreshed_reference
1320 return refreshed_reference
1321
1321
1322 def _needs_merge_state_refresh(self, pull_request, target_reference):
1322 def _needs_merge_state_refresh(self, pull_request, target_reference):
1323 return not(
1323 return not(
1324 pull_request.revisions and
1324 pull_request.revisions and
1325 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1325 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1326 target_reference.commit_id == pull_request._last_merge_target_rev)
1326 target_reference.commit_id == pull_request._last_merge_target_rev)
1327
1327
1328 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1328 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1329 workspace_id = self._workspace_id(pull_request)
1329 workspace_id = self._workspace_id(pull_request)
1330 source_vcs = pull_request.source_repo.scm_instance()
1330 source_vcs = pull_request.source_repo.scm_instance()
1331 repo_id = pull_request.target_repo.repo_id
1331 repo_id = pull_request.target_repo.repo_id
1332 use_rebase = self._use_rebase_for_merging(pull_request)
1332 use_rebase = self._use_rebase_for_merging(pull_request)
1333 close_branch = self._close_branch_before_merging(pull_request)
1333 close_branch = self._close_branch_before_merging(pull_request)
1334 merge_state = target_vcs.merge(
1334 merge_state = target_vcs.merge(
1335 repo_id, workspace_id,
1335 repo_id, workspace_id,
1336 target_reference, source_vcs, pull_request.source_ref_parts,
1336 target_reference, source_vcs, pull_request.source_ref_parts,
1337 dry_run=True, use_rebase=use_rebase,
1337 dry_run=True, use_rebase=use_rebase,
1338 close_branch=close_branch)
1338 close_branch=close_branch)
1339
1339
1340 # Do not store the response if there was an unknown error.
1340 # Do not store the response if there was an unknown error.
1341 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1341 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1342 pull_request._last_merge_source_rev = \
1342 pull_request._last_merge_source_rev = \
1343 pull_request.source_ref_parts.commit_id
1343 pull_request.source_ref_parts.commit_id
1344 pull_request._last_merge_target_rev = target_reference.commit_id
1344 pull_request._last_merge_target_rev = target_reference.commit_id
1345 pull_request.last_merge_status = merge_state.failure_reason
1345 pull_request.last_merge_status = merge_state.failure_reason
1346 pull_request.shadow_merge_ref = merge_state.merge_ref
1346 pull_request.shadow_merge_ref = merge_state.merge_ref
1347 Session().add(pull_request)
1347 Session().add(pull_request)
1348 Session().commit()
1348 Session().commit()
1349
1349
1350 return merge_state
1350 return merge_state
1351
1351
1352 def _workspace_id(self, pull_request):
1352 def _workspace_id(self, pull_request):
1353 workspace_id = 'pr-%s' % pull_request.pull_request_id
1353 workspace_id = 'pr-%s' % pull_request.pull_request_id
1354 return workspace_id
1354 return workspace_id
1355
1355
1356 def generate_repo_data(self, repo, commit_id=None, branch=None,
1356 def generate_repo_data(self, repo, commit_id=None, branch=None,
1357 bookmark=None, translator=None):
1357 bookmark=None, translator=None):
1358 from rhodecode.model.repo import RepoModel
1358 from rhodecode.model.repo import RepoModel
1359
1359
1360 all_refs, selected_ref = \
1360 all_refs, selected_ref = \
1361 self._get_repo_pullrequest_sources(
1361 self._get_repo_pullrequest_sources(
1362 repo.scm_instance(), commit_id=commit_id,
1362 repo.scm_instance(), commit_id=commit_id,
1363 branch=branch, bookmark=bookmark, translator=translator)
1363 branch=branch, bookmark=bookmark, translator=translator)
1364
1364
1365 refs_select2 = []
1365 refs_select2 = []
1366 for element in all_refs:
1366 for element in all_refs:
1367 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1367 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1368 refs_select2.append({'text': element[1], 'children': children})
1368 refs_select2.append({'text': element[1], 'children': children})
1369
1369
1370 return {
1370 return {
1371 'user': {
1371 'user': {
1372 'user_id': repo.user.user_id,
1372 'user_id': repo.user.user_id,
1373 'username': repo.user.username,
1373 'username': repo.user.username,
1374 'firstname': repo.user.first_name,
1374 'firstname': repo.user.first_name,
1375 'lastname': repo.user.last_name,
1375 'lastname': repo.user.last_name,
1376 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1376 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1377 },
1377 },
1378 'name': repo.repo_name,
1378 'name': repo.repo_name,
1379 'link': RepoModel().get_url(repo),
1379 'link': RepoModel().get_url(repo),
1380 'description': h.chop_at_smart(repo.description_safe, '\n'),
1380 'description': h.chop_at_smart(repo.description_safe, '\n'),
1381 'refs': {
1381 'refs': {
1382 'all_refs': all_refs,
1382 'all_refs': all_refs,
1383 'selected_ref': selected_ref,
1383 'selected_ref': selected_ref,
1384 'select2_refs': refs_select2
1384 'select2_refs': refs_select2
1385 }
1385 }
1386 }
1386 }
1387
1387
1388 def generate_pullrequest_title(self, source, source_ref, target):
1388 def generate_pullrequest_title(self, source, source_ref, target):
1389 return u'{source}#{at_ref} to {target}'.format(
1389 return u'{source}#{at_ref} to {target}'.format(
1390 source=source,
1390 source=source,
1391 at_ref=source_ref,
1391 at_ref=source_ref,
1392 target=target,
1392 target=target,
1393 )
1393 )
1394
1394
1395 def _cleanup_merge_workspace(self, pull_request):
1395 def _cleanup_merge_workspace(self, pull_request):
1396 # Merging related cleanup
1396 # Merging related cleanup
1397 repo_id = pull_request.target_repo.repo_id
1397 repo_id = pull_request.target_repo.repo_id
1398 target_scm = pull_request.target_repo.scm_instance()
1398 target_scm = pull_request.target_repo.scm_instance()
1399 workspace_id = self._workspace_id(pull_request)
1399 workspace_id = self._workspace_id(pull_request)
1400
1400
1401 try:
1401 try:
1402 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1402 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1403 except NotImplementedError:
1403 except NotImplementedError:
1404 pass
1404 pass
1405
1405
1406 def _get_repo_pullrequest_sources(
1406 def _get_repo_pullrequest_sources(
1407 self, repo, commit_id=None, branch=None, bookmark=None,
1407 self, repo, commit_id=None, branch=None, bookmark=None,
1408 translator=None):
1408 translator=None):
1409 """
1409 """
1410 Return a structure with repo's interesting commits, suitable for
1410 Return a structure with repo's interesting commits, suitable for
1411 the selectors in pullrequest controller
1411 the selectors in pullrequest controller
1412
1412
1413 :param commit_id: a commit that must be in the list somehow
1413 :param commit_id: a commit that must be in the list somehow
1414 and selected by default
1414 and selected by default
1415 :param branch: a branch that must be in the list and selected
1415 :param branch: a branch that must be in the list and selected
1416 by default - even if closed
1416 by default - even if closed
1417 :param bookmark: a bookmark that must be in the list and selected
1417 :param bookmark: a bookmark that must be in the list and selected
1418 """
1418 """
1419 _ = translator or get_current_request().translate
1419 _ = translator or get_current_request().translate
1420
1420
1421 commit_id = safe_str(commit_id) if commit_id else None
1421 commit_id = safe_str(commit_id) if commit_id else None
1422 branch = safe_str(branch) if branch else None
1422 branch = safe_str(branch) if branch else None
1423 bookmark = safe_str(bookmark) if bookmark else None
1423 bookmark = safe_str(bookmark) if bookmark else None
1424
1424
1425 selected = None
1425 selected = None
1426
1426
1427 # order matters: first source that has commit_id in it will be selected
1427 # order matters: first source that has commit_id in it will be selected
1428 sources = []
1428 sources = []
1429 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1429 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1430 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1430 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1431
1431
1432 if commit_id:
1432 if commit_id:
1433 ref_commit = (h.short_id(commit_id), commit_id)
1433 ref_commit = (h.short_id(commit_id), commit_id)
1434 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1434 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1435
1435
1436 sources.append(
1436 sources.append(
1437 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1437 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1438 )
1438 )
1439
1439
1440 groups = []
1440 groups = []
1441 for group_key, ref_list, group_name, match in sources:
1441 for group_key, ref_list, group_name, match in sources:
1442 group_refs = []
1442 group_refs = []
1443 for ref_name, ref_id in ref_list:
1443 for ref_name, ref_id in ref_list:
1444 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1444 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1445 group_refs.append((ref_key, ref_name))
1445 group_refs.append((ref_key, ref_name))
1446
1446
1447 if not selected:
1447 if not selected:
1448 if set([commit_id, match]) & set([ref_id, ref_name]):
1448 if set([commit_id, match]) & set([ref_id, ref_name]):
1449 selected = ref_key
1449 selected = ref_key
1450
1450
1451 if group_refs:
1451 if group_refs:
1452 groups.append((group_refs, group_name))
1452 groups.append((group_refs, group_name))
1453
1453
1454 if not selected:
1454 if not selected:
1455 ref = commit_id or branch or bookmark
1455 ref = commit_id or branch or bookmark
1456 if ref:
1456 if ref:
1457 raise CommitDoesNotExistError(
1457 raise CommitDoesNotExistError(
1458 'No commit refs could be found matching: %s' % ref)
1458 'No commit refs could be found matching: %s' % ref)
1459 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1459 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1460 selected = 'branch:%s:%s' % (
1460 selected = 'branch:%s:%s' % (
1461 repo.DEFAULT_BRANCH_NAME,
1461 repo.DEFAULT_BRANCH_NAME,
1462 repo.branches[repo.DEFAULT_BRANCH_NAME]
1462 repo.branches[repo.DEFAULT_BRANCH_NAME]
1463 )
1463 )
1464 elif repo.commit_ids:
1464 elif repo.commit_ids:
1465 # make the user select in this case
1465 # make the user select in this case
1466 selected = None
1466 selected = None
1467 else:
1467 else:
1468 raise EmptyRepositoryError()
1468 raise EmptyRepositoryError()
1469 return groups, selected
1469 return groups, selected
1470
1470
1471 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1471 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1472 hide_whitespace_changes, diff_context):
1472 hide_whitespace_changes, diff_context):
1473
1473
1474 return self._get_diff_from_pr_or_version(
1474 return self._get_diff_from_pr_or_version(
1475 source_repo, source_ref_id, target_ref_id,
1475 source_repo, source_ref_id, target_ref_id,
1476 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1476 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1477
1477
1478 def _get_diff_from_pr_or_version(
1478 def _get_diff_from_pr_or_version(
1479 self, source_repo, source_ref_id, target_ref_id,
1479 self, source_repo, source_ref_id, target_ref_id,
1480 hide_whitespace_changes, diff_context):
1480 hide_whitespace_changes, diff_context):
1481
1481
1482 target_commit = source_repo.get_commit(
1482 target_commit = source_repo.get_commit(
1483 commit_id=safe_str(target_ref_id))
1483 commit_id=safe_str(target_ref_id))
1484 source_commit = source_repo.get_commit(
1484 source_commit = source_repo.get_commit(
1485 commit_id=safe_str(source_ref_id))
1485 commit_id=safe_str(source_ref_id))
1486 if isinstance(source_repo, Repository):
1486 if isinstance(source_repo, Repository):
1487 vcs_repo = source_repo.scm_instance()
1487 vcs_repo = source_repo.scm_instance()
1488 else:
1488 else:
1489 vcs_repo = source_repo
1489 vcs_repo = source_repo
1490
1490
1491 # TODO: johbo: In the context of an update, we cannot reach
1491 # TODO: johbo: In the context of an update, we cannot reach
1492 # the old commit anymore with our normal mechanisms. It needs
1492 # the old commit anymore with our normal mechanisms. It needs
1493 # some sort of special support in the vcs layer to avoid this
1493 # some sort of special support in the vcs layer to avoid this
1494 # workaround.
1494 # workaround.
1495 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1495 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1496 vcs_repo.alias == 'git'):
1496 vcs_repo.alias == 'git'):
1497 source_commit.raw_id = safe_str(source_ref_id)
1497 source_commit.raw_id = safe_str(source_ref_id)
1498
1498
1499 log.debug('calculating diff between '
1499 log.debug('calculating diff between '
1500 'source_ref:%s and target_ref:%s for repo `%s`',
1500 'source_ref:%s and target_ref:%s for repo `%s`',
1501 target_ref_id, source_ref_id,
1501 target_ref_id, source_ref_id,
1502 safe_unicode(vcs_repo.path))
1502 safe_unicode(vcs_repo.path))
1503
1503
1504 vcs_diff = vcs_repo.get_diff(
1504 vcs_diff = vcs_repo.get_diff(
1505 commit1=target_commit, commit2=source_commit,
1505 commit1=target_commit, commit2=source_commit,
1506 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1506 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1507 return vcs_diff
1507 return vcs_diff
1508
1508
1509 def _is_merge_enabled(self, pull_request):
1509 def _is_merge_enabled(self, pull_request):
1510 return self._get_general_setting(
1510 return self._get_general_setting(
1511 pull_request, 'rhodecode_pr_merge_enabled')
1511 pull_request, 'rhodecode_pr_merge_enabled')
1512
1512
1513 def _use_rebase_for_merging(self, pull_request):
1513 def _use_rebase_for_merging(self, pull_request):
1514 repo_type = pull_request.target_repo.repo_type
1514 repo_type = pull_request.target_repo.repo_type
1515 if repo_type == 'hg':
1515 if repo_type == 'hg':
1516 return self._get_general_setting(
1516 return self._get_general_setting(
1517 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1517 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1518 elif repo_type == 'git':
1518 elif repo_type == 'git':
1519 return self._get_general_setting(
1519 return self._get_general_setting(
1520 pull_request, 'rhodecode_git_use_rebase_for_merging')
1520 pull_request, 'rhodecode_git_use_rebase_for_merging')
1521
1521
1522 return False
1522 return False
1523
1523
1524 def _close_branch_before_merging(self, pull_request):
1524 def _close_branch_before_merging(self, pull_request):
1525 repo_type = pull_request.target_repo.repo_type
1525 repo_type = pull_request.target_repo.repo_type
1526 if repo_type == 'hg':
1526 if repo_type == 'hg':
1527 return self._get_general_setting(
1527 return self._get_general_setting(
1528 pull_request, 'rhodecode_hg_close_branch_before_merging')
1528 pull_request, 'rhodecode_hg_close_branch_before_merging')
1529 elif repo_type == 'git':
1529 elif repo_type == 'git':
1530 return self._get_general_setting(
1530 return self._get_general_setting(
1531 pull_request, 'rhodecode_git_close_branch_before_merging')
1531 pull_request, 'rhodecode_git_close_branch_before_merging')
1532
1532
1533 return False
1533 return False
1534
1534
1535 def _get_general_setting(self, pull_request, settings_key, default=False):
1535 def _get_general_setting(self, pull_request, settings_key, default=False):
1536 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1536 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1537 settings = settings_model.get_general_settings()
1537 settings = settings_model.get_general_settings()
1538 return settings.get(settings_key, default)
1538 return settings.get(settings_key, default)
1539
1539
1540 def _log_audit_action(self, action, action_data, user, pull_request):
1540 def _log_audit_action(self, action, action_data, user, pull_request):
1541 audit_logger.store(
1541 audit_logger.store(
1542 action=action,
1542 action=action,
1543 action_data=action_data,
1543 action_data=action_data,
1544 user=user,
1544 user=user,
1545 repo=pull_request.target_repo)
1545 repo=pull_request.target_repo)
1546
1546
1547 def get_reviewer_functions(self):
1547 def get_reviewer_functions(self):
1548 """
1548 """
1549 Fetches functions for validation and fetching default reviewers.
1549 Fetches functions for validation and fetching default reviewers.
1550 If available we use the EE package, else we fallback to CE
1550 If available we use the EE package, else we fallback to CE
1551 package functions
1551 package functions
1552 """
1552 """
1553 try:
1553 try:
1554 from rc_reviewers.utils import get_default_reviewers_data
1554 from rc_reviewers.utils import get_default_reviewers_data
1555 from rc_reviewers.utils import validate_default_reviewers
1555 from rc_reviewers.utils import validate_default_reviewers
1556 except ImportError:
1556 except ImportError:
1557 from rhodecode.apps.repository.utils import get_default_reviewers_data
1557 from rhodecode.apps.repository.utils import get_default_reviewers_data
1558 from rhodecode.apps.repository.utils import validate_default_reviewers
1558 from rhodecode.apps.repository.utils import validate_default_reviewers
1559
1559
1560 return get_default_reviewers_data, validate_default_reviewers
1560 return get_default_reviewers_data, validate_default_reviewers
1561
1561
1562
1562
1563 class MergeCheck(object):
1563 class MergeCheck(object):
1564 """
1564 """
1565 Perform Merge Checks and returns a check object which stores information
1565 Perform Merge Checks and returns a check object which stores information
1566 about merge errors, and merge conditions
1566 about merge errors, and merge conditions
1567 """
1567 """
1568 TODO_CHECK = 'todo'
1568 TODO_CHECK = 'todo'
1569 PERM_CHECK = 'perm'
1569 PERM_CHECK = 'perm'
1570 REVIEW_CHECK = 'review'
1570 REVIEW_CHECK = 'review'
1571 MERGE_CHECK = 'merge'
1571 MERGE_CHECK = 'merge'
1572
1572
1573 def __init__(self):
1573 def __init__(self):
1574 self.review_status = None
1574 self.review_status = None
1575 self.merge_possible = None
1575 self.merge_possible = None
1576 self.merge_msg = ''
1576 self.merge_msg = ''
1577 self.failed = None
1577 self.failed = None
1578 self.errors = []
1578 self.errors = []
1579 self.error_details = OrderedDict()
1579 self.error_details = OrderedDict()
1580
1580
1581 def push_error(self, error_type, message, error_key, details):
1581 def push_error(self, error_type, message, error_key, details):
1582 self.failed = True
1582 self.failed = True
1583 self.errors.append([error_type, message])
1583 self.errors.append([error_type, message])
1584 self.error_details[error_key] = dict(
1584 self.error_details[error_key] = dict(
1585 details=details,
1585 details=details,
1586 error_type=error_type,
1586 error_type=error_type,
1587 message=message
1587 message=message
1588 )
1588 )
1589
1589
1590 @classmethod
1590 @classmethod
1591 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1591 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1592 force_shadow_repo_refresh=False):
1592 force_shadow_repo_refresh=False):
1593 _ = translator
1593 _ = translator
1594 merge_check = cls()
1594 merge_check = cls()
1595
1595
1596 # permissions to merge
1596 # permissions to merge
1597 user_allowed_to_merge = PullRequestModel().check_user_merge(
1597 user_allowed_to_merge = PullRequestModel().check_user_merge(
1598 pull_request, auth_user)
1598 pull_request, auth_user)
1599 if not user_allowed_to_merge:
1599 if not user_allowed_to_merge:
1600 log.debug("MergeCheck: cannot merge, approval is pending.")
1600 log.debug("MergeCheck: cannot merge, approval is pending.")
1601
1601
1602 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1602 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1603 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1603 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1604 if fail_early:
1604 if fail_early:
1605 return merge_check
1605 return merge_check
1606
1606
1607 # permission to merge into the target branch
1607 # permission to merge into the target branch
1608 target_commit_id = pull_request.target_ref_parts.commit_id
1608 target_commit_id = pull_request.target_ref_parts.commit_id
1609 if pull_request.target_ref_parts.type == 'branch':
1609 if pull_request.target_ref_parts.type == 'branch':
1610 branch_name = pull_request.target_ref_parts.name
1610 branch_name = pull_request.target_ref_parts.name
1611 else:
1611 else:
1612 # for mercurial we can always figure out the branch from the commit
1612 # for mercurial we can always figure out the branch from the commit
1613 # in case of bookmark
1613 # in case of bookmark
1614 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1614 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1615 branch_name = target_commit.branch
1615 branch_name = target_commit.branch
1616
1616
1617 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1617 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1618 pull_request.target_repo.repo_name, branch_name)
1618 pull_request.target_repo.repo_name, branch_name)
1619 if branch_perm and branch_perm == 'branch.none':
1619 if branch_perm and branch_perm == 'branch.none':
1620 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1620 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1621 branch_name, rule)
1621 branch_name, rule)
1622 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1622 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1623 if fail_early:
1623 if fail_early:
1624 return merge_check
1624 return merge_check
1625
1625
1626 # review status, must be always present
1626 # review status, must be always present
1627 review_status = pull_request.calculated_review_status()
1627 review_status = pull_request.calculated_review_status()
1628 merge_check.review_status = review_status
1628 merge_check.review_status = review_status
1629
1629
1630 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1630 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1631 if not status_approved:
1631 if not status_approved:
1632 log.debug("MergeCheck: cannot merge, approval is pending.")
1632 log.debug("MergeCheck: cannot merge, approval is pending.")
1633
1633
1634 msg = _('Pull request reviewer approval is pending.')
1634 msg = _('Pull request reviewer approval is pending.')
1635
1635
1636 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1636 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1637
1637
1638 if fail_early:
1638 if fail_early:
1639 return merge_check
1639 return merge_check
1640
1640
1641 # left over TODOs
1641 # left over TODOs
1642 todos = CommentsModel().get_unresolved_todos(pull_request)
1642 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1643 if todos:
1643 if todos:
1644 log.debug("MergeCheck: cannot merge, {} "
1644 log.debug("MergeCheck: cannot merge, {} "
1645 "unresolved TODOs left.".format(len(todos)))
1645 "unresolved TODOs left.".format(len(todos)))
1646
1646
1647 if len(todos) == 1:
1647 if len(todos) == 1:
1648 msg = _('Cannot merge, {} TODO still not resolved.').format(
1648 msg = _('Cannot merge, {} TODO still not resolved.').format(
1649 len(todos))
1649 len(todos))
1650 else:
1650 else:
1651 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1651 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1652 len(todos))
1652 len(todos))
1653
1653
1654 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1654 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1655
1655
1656 if fail_early:
1656 if fail_early:
1657 return merge_check
1657 return merge_check
1658
1658
1659 # merge possible, here is the filesystem simulation + shadow repo
1659 # merge possible, here is the filesystem simulation + shadow repo
1660 merge_status, msg = PullRequestModel().merge_status(
1660 merge_status, msg = PullRequestModel().merge_status(
1661 pull_request, translator=translator,
1661 pull_request, translator=translator,
1662 force_shadow_repo_refresh=force_shadow_repo_refresh)
1662 force_shadow_repo_refresh=force_shadow_repo_refresh)
1663 merge_check.merge_possible = merge_status
1663 merge_check.merge_possible = merge_status
1664 merge_check.merge_msg = msg
1664 merge_check.merge_msg = msg
1665 if not merge_status:
1665 if not merge_status:
1666 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1666 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1667 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1667 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1668
1668
1669 if fail_early:
1669 if fail_early:
1670 return merge_check
1670 return merge_check
1671
1671
1672 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1672 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1673 return merge_check
1673 return merge_check
1674
1674
1675 @classmethod
1675 @classmethod
1676 def get_merge_conditions(cls, pull_request, translator):
1676 def get_merge_conditions(cls, pull_request, translator):
1677 _ = translator
1677 _ = translator
1678 merge_details = {}
1678 merge_details = {}
1679
1679
1680 model = PullRequestModel()
1680 model = PullRequestModel()
1681 use_rebase = model._use_rebase_for_merging(pull_request)
1681 use_rebase = model._use_rebase_for_merging(pull_request)
1682
1682
1683 if use_rebase:
1683 if use_rebase:
1684 merge_details['merge_strategy'] = dict(
1684 merge_details['merge_strategy'] = dict(
1685 details={},
1685 details={},
1686 message=_('Merge strategy: rebase')
1686 message=_('Merge strategy: rebase')
1687 )
1687 )
1688 else:
1688 else:
1689 merge_details['merge_strategy'] = dict(
1689 merge_details['merge_strategy'] = dict(
1690 details={},
1690 details={},
1691 message=_('Merge strategy: explicit merge commit')
1691 message=_('Merge strategy: explicit merge commit')
1692 )
1692 )
1693
1693
1694 close_branch = model._close_branch_before_merging(pull_request)
1694 close_branch = model._close_branch_before_merging(pull_request)
1695 if close_branch:
1695 if close_branch:
1696 repo_type = pull_request.target_repo.repo_type
1696 repo_type = pull_request.target_repo.repo_type
1697 close_msg = ''
1697 close_msg = ''
1698 if repo_type == 'hg':
1698 if repo_type == 'hg':
1699 close_msg = _('Source branch will be closed after merge.')
1699 close_msg = _('Source branch will be closed after merge.')
1700 elif repo_type == 'git':
1700 elif repo_type == 'git':
1701 close_msg = _('Source branch will be deleted after merge.')
1701 close_msg = _('Source branch will be deleted after merge.')
1702
1702
1703 merge_details['close_branch'] = dict(
1703 merge_details['close_branch'] = dict(
1704 details={},
1704 details={},
1705 message=close_msg
1705 message=close_msg
1706 )
1706 )
1707
1707
1708 return merge_details
1708 return merge_details
1709
1709
1710
1710
1711 ChangeTuple = collections.namedtuple(
1711 ChangeTuple = collections.namedtuple(
1712 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1712 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1713
1713
1714 FileChangeTuple = collections.namedtuple(
1714 FileChangeTuple = collections.namedtuple(
1715 'FileChangeTuple', ['added', 'modified', 'removed'])
1715 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now