##// END OF EJS Templates
pull-requests: moved get_pr_version into PullRequestModel.
marcink -
r2393:e9838883 default
parent child Browse files
Show More
@@ -1,1259 +1,1236 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'rhodecode:templates/data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176 self.load_default_context()
177 177
178 178 # additional filters
179 179 req_get = self.request.GET
180 180 source = str2bool(req_get.get('source'))
181 181 closed = str2bool(req_get.get('closed'))
182 182 my = str2bool(req_get.get('my'))
183 183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 185
186 186 filter_type = 'awaiting_review' if awaiting_review \
187 187 else 'awaiting_my_review' if awaiting_my_review \
188 188 else None
189 189
190 190 opened_by = None
191 191 if my:
192 192 opened_by = [self._rhodecode_user.user_id]
193 193
194 194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 195 if closed:
196 196 statuses = [PullRequest.STATUS_CLOSED]
197 197
198 198 data = self._get_pull_requests_list(
199 199 repo_name=self.db_repo_name, source=source,
200 200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 201
202 202 return data
203 203
204 def _get_pr_version(self, pull_request_id, version=None):
205 at_version = None
206
207 if version and version == 'latest':
208 pull_request_ver = PullRequest.get(pull_request_id)
209 pull_request_obj = pull_request_ver
210 _org_pull_request_obj = pull_request_obj
211 at_version = 'latest'
212 elif version:
213 pull_request_ver = PullRequestVersion.get_or_404(version)
214 pull_request_obj = pull_request_ver
215 _org_pull_request_obj = pull_request_ver.pull_request
216 at_version = pull_request_ver.pull_request_version_id
217 else:
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
219 pull_request_id)
220
221 pull_request_display_obj = PullRequest.get_pr_display_object(
222 pull_request_obj, _org_pull_request_obj)
223
224 return _org_pull_request_obj, pull_request_obj, \
225 pull_request_display_obj, at_version
226
227 204 def _get_diffset(self, source_repo_name, source_repo,
228 205 source_ref_id, target_ref_id,
229 206 target_commit, source_commit, diff_limit, fulldiff,
230 207 file_limit, display_inline_comments):
231 208
232 209 vcs_diff = PullRequestModel().get_diff(
233 210 source_repo, source_ref_id, target_ref_id)
234 211
235 212 diff_processor = diffs.DiffProcessor(
236 213 vcs_diff, format='newdiff', diff_limit=diff_limit,
237 214 file_limit=file_limit, show_full_diff=fulldiff)
238 215
239 216 _parsed = diff_processor.prepare()
240 217
241 218 def _node_getter(commit):
242 219 def get_node(fname):
243 220 try:
244 221 return commit.get_node(fname)
245 222 except NodeDoesNotExistError:
246 223 return None
247 224
248 225 return get_node
249 226
250 227 diffset = codeblocks.DiffSet(
251 228 repo_name=self.db_repo_name,
252 229 source_repo_name=source_repo_name,
253 230 source_node_getter=_node_getter(target_commit),
254 231 target_node_getter=_node_getter(source_commit),
255 232 comments=display_inline_comments
256 233 )
257 234 diffset = diffset.render_patchset(
258 235 _parsed, target_commit.raw_id, source_commit.raw_id)
259 236
260 237 return diffset
261 238
262 239 @LoginRequired()
263 240 @HasRepoPermissionAnyDecorator(
264 241 'repository.read', 'repository.write', 'repository.admin')
265 242 @view_config(
266 243 route_name='pullrequest_show', request_method='GET',
267 244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 245 def pull_request_show(self):
269 246 pull_request_id = self.request.matchdict['pull_request_id']
270 247
271 248 c = self.load_default_context()
272 249
273 250 version = self.request.GET.get('version')
274 251 from_version = self.request.GET.get('from_version') or version
275 252 merge_checks = self.request.GET.get('merge_checks')
276 253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
277 254
278 255 (pull_request_latest,
279 256 pull_request_at_ver,
280 257 pull_request_display_obj,
281 at_version) = self._get_pr_version(
258 at_version) = PullRequestModel().get_pr_version(
282 259 pull_request_id, version=version)
283 260 pr_closed = pull_request_latest.is_closed()
284 261
285 262 if pr_closed and (version or from_version):
286 263 # not allow to browse versions
287 264 raise HTTPFound(h.route_path(
288 265 'pullrequest_show', repo_name=self.db_repo_name,
289 266 pull_request_id=pull_request_id))
290 267
291 268 versions = pull_request_display_obj.versions()
292 269
293 270 c.at_version = at_version
294 271 c.at_version_num = (at_version
295 272 if at_version and at_version != 'latest'
296 273 else None)
297 274 c.at_version_pos = ChangesetComment.get_index_from_version(
298 275 c.at_version_num, versions)
299 276
300 277 (prev_pull_request_latest,
301 278 prev_pull_request_at_ver,
302 279 prev_pull_request_display_obj,
303 prev_at_version) = self._get_pr_version(
280 prev_at_version) = PullRequestModel().get_pr_version(
304 281 pull_request_id, version=from_version)
305 282
306 283 c.from_version = prev_at_version
307 284 c.from_version_num = (prev_at_version
308 285 if prev_at_version and prev_at_version != 'latest'
309 286 else None)
310 287 c.from_version_pos = ChangesetComment.get_index_from_version(
311 288 c.from_version_num, versions)
312 289
313 290 # define if we're in COMPARE mode or VIEW at version mode
314 291 compare = at_version != prev_at_version
315 292
316 293 # pull_requests repo_name we opened it against
317 294 # ie. target_repo must match
318 295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
319 296 raise HTTPNotFound()
320 297
321 298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
322 299 pull_request_at_ver)
323 300
324 301 c.pull_request = pull_request_display_obj
325 302 c.pull_request_latest = pull_request_latest
326 303
327 304 if compare or (at_version and not at_version == 'latest'):
328 305 c.allowed_to_change_status = False
329 306 c.allowed_to_update = False
330 307 c.allowed_to_merge = False
331 308 c.allowed_to_delete = False
332 309 c.allowed_to_comment = False
333 310 c.allowed_to_close = False
334 311 else:
335 312 can_change_status = PullRequestModel().check_user_change_status(
336 313 pull_request_at_ver, self._rhodecode_user)
337 314 c.allowed_to_change_status = can_change_status and not pr_closed
338 315
339 316 c.allowed_to_update = PullRequestModel().check_user_update(
340 317 pull_request_latest, self._rhodecode_user) and not pr_closed
341 318 c.allowed_to_merge = PullRequestModel().check_user_merge(
342 319 pull_request_latest, self._rhodecode_user) and not pr_closed
343 320 c.allowed_to_delete = PullRequestModel().check_user_delete(
344 321 pull_request_latest, self._rhodecode_user) and not pr_closed
345 322 c.allowed_to_comment = not pr_closed
346 323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
347 324
348 325 c.forbid_adding_reviewers = False
349 326 c.forbid_author_to_review = False
350 327 c.forbid_commit_author_to_review = False
351 328
352 329 if pull_request_latest.reviewer_data and \
353 330 'rules' in pull_request_latest.reviewer_data:
354 331 rules = pull_request_latest.reviewer_data['rules'] or {}
355 332 try:
356 333 c.forbid_adding_reviewers = rules.get(
357 334 'forbid_adding_reviewers')
358 335 c.forbid_author_to_review = rules.get(
359 336 'forbid_author_to_review')
360 337 c.forbid_commit_author_to_review = rules.get(
361 338 'forbid_commit_author_to_review')
362 339 except Exception:
363 340 pass
364 341
365 342 # check merge capabilities
366 343 _merge_check = MergeCheck.validate(
367 344 pull_request_latest, user=self._rhodecode_user,
368 345 translator=self.request.translate)
369 346 c.pr_merge_errors = _merge_check.error_details
370 347 c.pr_merge_possible = not _merge_check.failed
371 348 c.pr_merge_message = _merge_check.merge_msg
372 349
373 350 c.pr_merge_info = MergeCheck.get_merge_conditions(
374 351 pull_request_latest, translator=self.request.translate)
375 352
376 353 c.pull_request_review_status = _merge_check.review_status
377 354 if merge_checks:
378 355 self.request.override_renderer = \
379 356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
380 357 return self._get_template_context(c)
381 358
382 359 comments_model = CommentsModel()
383 360
384 361 # reviewers and statuses
385 362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
386 363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
387 364
388 365 # GENERAL COMMENTS with versions #
389 366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
390 367 q = q.order_by(ChangesetComment.comment_id.asc())
391 368 general_comments = q
392 369
393 370 # pick comments we want to render at current version
394 371 c.comment_versions = comments_model.aggregate_comments(
395 372 general_comments, versions, c.at_version_num)
396 373 c.comments = c.comment_versions[c.at_version_num]['until']
397 374
398 375 # INLINE COMMENTS with versions #
399 376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
400 377 q = q.order_by(ChangesetComment.comment_id.asc())
401 378 inline_comments = q
402 379
403 380 c.inline_versions = comments_model.aggregate_comments(
404 381 inline_comments, versions, c.at_version_num, inline=True)
405 382
406 383 # inject latest version
407 384 latest_ver = PullRequest.get_pr_display_object(
408 385 pull_request_latest, pull_request_latest)
409 386
410 387 c.versions = versions + [latest_ver]
411 388
412 389 # if we use version, then do not show later comments
413 390 # than current version
414 391 display_inline_comments = collections.defaultdict(
415 392 lambda: collections.defaultdict(list))
416 393 for co in inline_comments:
417 394 if c.at_version_num:
418 395 # pick comments that are at least UPTO given version, so we
419 396 # don't render comments for higher version
420 397 should_render = co.pull_request_version_id and \
421 398 co.pull_request_version_id <= c.at_version_num
422 399 else:
423 400 # showing all, for 'latest'
424 401 should_render = True
425 402
426 403 if should_render:
427 404 display_inline_comments[co.f_path][co.line_no].append(co)
428 405
429 406 # load diff data into template context, if we use compare mode then
430 407 # diff is calculated based on changes between versions of PR
431 408
432 409 source_repo = pull_request_at_ver.source_repo
433 410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
434 411
435 412 target_repo = pull_request_at_ver.target_repo
436 413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
437 414
438 415 if compare:
439 416 # in compare switch the diff base to latest commit from prev version
440 417 target_ref_id = prev_pull_request_display_obj.revisions[0]
441 418
442 419 # despite opening commits for bookmarks/branches/tags, we always
443 420 # convert this to rev to prevent changes after bookmark or branch change
444 421 c.source_ref_type = 'rev'
445 422 c.source_ref = source_ref_id
446 423
447 424 c.target_ref_type = 'rev'
448 425 c.target_ref = target_ref_id
449 426
450 427 c.source_repo = source_repo
451 428 c.target_repo = target_repo
452 429
453 430 c.commit_ranges = []
454 431 source_commit = EmptyCommit()
455 432 target_commit = EmptyCommit()
456 433 c.missing_requirements = False
457 434
458 435 source_scm = source_repo.scm_instance()
459 436 target_scm = target_repo.scm_instance()
460 437
461 438 # try first shadow repo, fallback to regular repo
462 439 try:
463 440 commits_source_repo = pull_request_latest.get_shadow_repo()
464 441 except Exception:
465 442 log.debug('Failed to get shadow repo', exc_info=True)
466 443 commits_source_repo = source_scm
467 444
468 445 c.commits_source_repo = commits_source_repo
469 446 commit_cache = {}
470 447 try:
471 448 pre_load = ["author", "branch", "date", "message"]
472 449 show_revs = pull_request_at_ver.revisions
473 450 for rev in show_revs:
474 451 comm = commits_source_repo.get_commit(
475 452 commit_id=rev, pre_load=pre_load)
476 453 c.commit_ranges.append(comm)
477 454 commit_cache[comm.raw_id] = comm
478 455
479 456 # Order here matters, we first need to get target, and then
480 457 # the source
481 458 target_commit = commits_source_repo.get_commit(
482 459 commit_id=safe_str(target_ref_id))
483 460
484 461 source_commit = commits_source_repo.get_commit(
485 462 commit_id=safe_str(source_ref_id))
486 463
487 464 except CommitDoesNotExistError:
488 465 log.warning(
489 466 'Failed to get commit from `{}` repo'.format(
490 467 commits_source_repo), exc_info=True)
491 468 except RepositoryRequirementError:
492 469 log.warning(
493 470 'Failed to get all required data from repo', exc_info=True)
494 471 c.missing_requirements = True
495 472
496 473 c.ancestor = None # set it to None, to hide it from PR view
497 474
498 475 try:
499 476 ancestor_id = source_scm.get_common_ancestor(
500 477 source_commit.raw_id, target_commit.raw_id, target_scm)
501 478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
502 479 except Exception:
503 480 c.ancestor_commit = None
504 481
505 482 c.statuses = source_repo.statuses(
506 483 [x.raw_id for x in c.commit_ranges])
507 484
508 485 # auto collapse if we have more than limit
509 486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
510 487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
511 488 c.compare_mode = compare
512 489
513 490 # diff_limit is the old behavior, will cut off the whole diff
514 491 # if the limit is applied otherwise will just hide the
515 492 # big files from the front-end
516 493 diff_limit = c.visual.cut_off_limit_diff
517 494 file_limit = c.visual.cut_off_limit_file
518 495
519 496 c.missing_commits = False
520 497 if (c.missing_requirements
521 498 or isinstance(source_commit, EmptyCommit)
522 499 or source_commit == target_commit):
523 500
524 501 c.missing_commits = True
525 502 else:
526 503
527 504 c.diffset = self._get_diffset(
528 505 c.source_repo.repo_name, commits_source_repo,
529 506 source_ref_id, target_ref_id,
530 507 target_commit, source_commit,
531 508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
532 509
533 510 c.limited_diff = c.diffset.limited_diff
534 511
535 512 # calculate removed files that are bound to comments
536 513 comment_deleted_files = [
537 514 fname for fname in display_inline_comments
538 515 if fname not in c.diffset.file_stats]
539 516
540 517 c.deleted_files_comments = collections.defaultdict(dict)
541 518 for fname, per_line_comments in display_inline_comments.items():
542 519 if fname in comment_deleted_files:
543 520 c.deleted_files_comments[fname]['stats'] = 0
544 521 c.deleted_files_comments[fname]['comments'] = list()
545 522 for lno, comments in per_line_comments.items():
546 523 c.deleted_files_comments[fname]['comments'].extend(
547 524 comments)
548 525
549 526 # this is a hack to properly display links, when creating PR, the
550 527 # compare view and others uses different notation, and
551 528 # compare_commits.mako renders links based on the target_repo.
552 529 # We need to swap that here to generate it properly on the html side
553 530 c.target_repo = c.source_repo
554 531
555 532 c.commit_statuses = ChangesetStatus.STATUSES
556 533
557 534 c.show_version_changes = not pr_closed
558 535 if c.show_version_changes:
559 536 cur_obj = pull_request_at_ver
560 537 prev_obj = prev_pull_request_at_ver
561 538
562 539 old_commit_ids = prev_obj.revisions
563 540 new_commit_ids = cur_obj.revisions
564 541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
565 542 old_commit_ids, new_commit_ids)
566 543 c.commit_changes_summary = commit_changes
567 544
568 545 # calculate the diff for commits between versions
569 546 c.commit_changes = []
570 547 mark = lambda cs, fw: list(
571 548 h.itertools.izip_longest([], cs, fillvalue=fw))
572 549 for c_type, raw_id in mark(commit_changes.added, 'a') \
573 550 + mark(commit_changes.removed, 'r') \
574 551 + mark(commit_changes.common, 'c'):
575 552
576 553 if raw_id in commit_cache:
577 554 commit = commit_cache[raw_id]
578 555 else:
579 556 try:
580 557 commit = commits_source_repo.get_commit(raw_id)
581 558 except CommitDoesNotExistError:
582 559 # in case we fail extracting still use "dummy" commit
583 560 # for display in commit diff
584 561 commit = h.AttributeDict(
585 562 {'raw_id': raw_id,
586 563 'message': 'EMPTY or MISSING COMMIT'})
587 564 c.commit_changes.append([c_type, commit])
588 565
589 566 # current user review statuses for each version
590 567 c.review_versions = {}
591 568 if self._rhodecode_user.user_id in allowed_reviewers:
592 569 for co in general_comments:
593 570 if co.author.user_id == self._rhodecode_user.user_id:
594 571 # each comment has a status change
595 572 status = co.status_change
596 573 if status:
597 574 _ver_pr = status[0].comment.pull_request_version_id
598 575 c.review_versions[_ver_pr] = status[0]
599 576
600 577 return self._get_template_context(c)
601 578
602 579 def assure_not_empty_repo(self):
603 580 _ = self.request.translate
604 581
605 582 try:
606 583 self.db_repo.scm_instance().get_commit()
607 584 except EmptyRepositoryError:
608 585 h.flash(h.literal(_('There are no commits yet')),
609 586 category='warning')
610 587 raise HTTPFound(
611 588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
612 589
613 590 @LoginRequired()
614 591 @NotAnonymous()
615 592 @HasRepoPermissionAnyDecorator(
616 593 'repository.read', 'repository.write', 'repository.admin')
617 594 @view_config(
618 595 route_name='pullrequest_new', request_method='GET',
619 596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
620 597 def pull_request_new(self):
621 598 _ = self.request.translate
622 599 c = self.load_default_context()
623 600
624 601 self.assure_not_empty_repo()
625 602 source_repo = self.db_repo
626 603
627 604 commit_id = self.request.GET.get('commit')
628 605 branch_ref = self.request.GET.get('branch')
629 606 bookmark_ref = self.request.GET.get('bookmark')
630 607
631 608 try:
632 609 source_repo_data = PullRequestModel().generate_repo_data(
633 610 source_repo, commit_id=commit_id,
634 611 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
635 612 except CommitDoesNotExistError as e:
636 613 log.exception(e)
637 614 h.flash(_('Commit does not exist'), 'error')
638 615 raise HTTPFound(
639 616 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
640 617
641 618 default_target_repo = source_repo
642 619
643 620 if source_repo.parent:
644 621 parent_vcs_obj = source_repo.parent.scm_instance()
645 622 if parent_vcs_obj and not parent_vcs_obj.is_empty():
646 623 # change default if we have a parent repo
647 624 default_target_repo = source_repo.parent
648 625
649 626 target_repo_data = PullRequestModel().generate_repo_data(
650 627 default_target_repo, translator=self.request.translate)
651 628
652 629 selected_source_ref = source_repo_data['refs']['selected_ref']
653 630
654 631 title_source_ref = selected_source_ref.split(':', 2)[1]
655 632 c.default_title = PullRequestModel().generate_pullrequest_title(
656 633 source=source_repo.repo_name,
657 634 source_ref=title_source_ref,
658 635 target=default_target_repo.repo_name
659 636 )
660 637
661 638 c.default_repo_data = {
662 639 'source_repo_name': source_repo.repo_name,
663 640 'source_refs_json': json.dumps(source_repo_data),
664 641 'target_repo_name': default_target_repo.repo_name,
665 642 'target_refs_json': json.dumps(target_repo_data),
666 643 }
667 644 c.default_source_ref = selected_source_ref
668 645
669 646 return self._get_template_context(c)
670 647
671 648 @LoginRequired()
672 649 @NotAnonymous()
673 650 @HasRepoPermissionAnyDecorator(
674 651 'repository.read', 'repository.write', 'repository.admin')
675 652 @view_config(
676 653 route_name='pullrequest_repo_refs', request_method='GET',
677 654 renderer='json_ext', xhr=True)
678 655 def pull_request_repo_refs(self):
679 656 self.load_default_context()
680 657 target_repo_name = self.request.matchdict['target_repo_name']
681 658 repo = Repository.get_by_repo_name(target_repo_name)
682 659 if not repo:
683 660 raise HTTPNotFound()
684 661
685 662 target_perm = HasRepoPermissionAny(
686 663 'repository.read', 'repository.write', 'repository.admin')(
687 664 target_repo_name)
688 665 if not target_perm:
689 666 raise HTTPNotFound()
690 667
691 668 return PullRequestModel().generate_repo_data(
692 669 repo, translator=self.request.translate)
693 670
694 671 @LoginRequired()
695 672 @NotAnonymous()
696 673 @HasRepoPermissionAnyDecorator(
697 674 'repository.read', 'repository.write', 'repository.admin')
698 675 @view_config(
699 676 route_name='pullrequest_repo_destinations', request_method='GET',
700 677 renderer='json_ext', xhr=True)
701 678 def pull_request_repo_destinations(self):
702 679 _ = self.request.translate
703 680 filter_query = self.request.GET.get('query')
704 681
705 682 query = Repository.query() \
706 683 .order_by(func.length(Repository.repo_name)) \
707 684 .filter(
708 685 or_(Repository.repo_name == self.db_repo.repo_name,
709 686 Repository.fork_id == self.db_repo.repo_id))
710 687
711 688 if filter_query:
712 689 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
713 690 query = query.filter(
714 691 Repository.repo_name.ilike(ilike_expression))
715 692
716 693 add_parent = False
717 694 if self.db_repo.parent:
718 695 if filter_query in self.db_repo.parent.repo_name:
719 696 parent_vcs_obj = self.db_repo.parent.scm_instance()
720 697 if parent_vcs_obj and not parent_vcs_obj.is_empty():
721 698 add_parent = True
722 699
723 700 limit = 20 - 1 if add_parent else 20
724 701 all_repos = query.limit(limit).all()
725 702 if add_parent:
726 703 all_repos += [self.db_repo.parent]
727 704
728 705 repos = []
729 706 for obj in ScmModel().get_repos(all_repos):
730 707 repos.append({
731 708 'id': obj['name'],
732 709 'text': obj['name'],
733 710 'type': 'repo',
734 711 'obj': obj['dbrepo']
735 712 })
736 713
737 714 data = {
738 715 'more': False,
739 716 'results': [{
740 717 'text': _('Repositories'),
741 718 'children': repos
742 719 }] if repos else []
743 720 }
744 721 return data
745 722
746 723 @LoginRequired()
747 724 @NotAnonymous()
748 725 @HasRepoPermissionAnyDecorator(
749 726 'repository.read', 'repository.write', 'repository.admin')
750 727 @CSRFRequired()
751 728 @view_config(
752 729 route_name='pullrequest_create', request_method='POST',
753 730 renderer=None)
754 731 def pull_request_create(self):
755 732 _ = self.request.translate
756 733 self.assure_not_empty_repo()
757 734 self.load_default_context()
758 735
759 736 controls = peppercorn.parse(self.request.POST.items())
760 737
761 738 try:
762 739 form = PullRequestForm(
763 740 self.request.translate, self.db_repo.repo_id)()
764 741 _form = form.to_python(controls)
765 742 except formencode.Invalid as errors:
766 743 if errors.error_dict.get('revisions'):
767 744 msg = 'Revisions: %s' % errors.error_dict['revisions']
768 745 elif errors.error_dict.get('pullrequest_title'):
769 746 msg = _('Pull request requires a title with min. 3 chars')
770 747 else:
771 748 msg = _('Error creating pull request: {}').format(errors)
772 749 log.exception(msg)
773 750 h.flash(msg, 'error')
774 751
775 752 # would rather just go back to form ...
776 753 raise HTTPFound(
777 754 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
778 755
779 756 source_repo = _form['source_repo']
780 757 source_ref = _form['source_ref']
781 758 target_repo = _form['target_repo']
782 759 target_ref = _form['target_ref']
783 760 commit_ids = _form['revisions'][::-1]
784 761
785 762 # find the ancestor for this pr
786 763 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
787 764 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
788 765
789 766 # re-check permissions again here
790 767 # source_repo we must have read permissions
791 768
792 769 source_perm = HasRepoPermissionAny(
793 770 'repository.read',
794 771 'repository.write', 'repository.admin')(source_db_repo.repo_name)
795 772 if not source_perm:
796 773 msg = _('Not Enough permissions to source repo `{}`.'.format(
797 774 source_db_repo.repo_name))
798 775 h.flash(msg, category='error')
799 776 # copy the args back to redirect
800 777 org_query = self.request.GET.mixed()
801 778 raise HTTPFound(
802 779 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
803 780 _query=org_query))
804 781
805 782 # target repo we must have read permissions, and also later on
806 783 # we want to check branch permissions here
807 784 target_perm = HasRepoPermissionAny(
808 785 'repository.read',
809 786 'repository.write', 'repository.admin')(target_db_repo.repo_name)
810 787 if not target_perm:
811 788 msg = _('Not Enough permissions to target repo `{}`.'.format(
812 789 target_db_repo.repo_name))
813 790 h.flash(msg, category='error')
814 791 # copy the args back to redirect
815 792 org_query = self.request.GET.mixed()
816 793 raise HTTPFound(
817 794 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
818 795 _query=org_query))
819 796
820 797 source_scm = source_db_repo.scm_instance()
821 798 target_scm = target_db_repo.scm_instance()
822 799
823 800 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
824 801 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
825 802
826 803 ancestor = source_scm.get_common_ancestor(
827 804 source_commit.raw_id, target_commit.raw_id, target_scm)
828 805
829 806 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
830 807 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
831 808
832 809 pullrequest_title = _form['pullrequest_title']
833 810 title_source_ref = source_ref.split(':', 2)[1]
834 811 if not pullrequest_title:
835 812 pullrequest_title = PullRequestModel().generate_pullrequest_title(
836 813 source=source_repo,
837 814 source_ref=title_source_ref,
838 815 target=target_repo
839 816 )
840 817
841 818 description = _form['pullrequest_desc']
842 819
843 820 get_default_reviewers_data, validate_default_reviewers = \
844 821 PullRequestModel().get_reviewer_functions()
845 822
846 823 # recalculate reviewers logic, to make sure we can validate this
847 824 reviewer_rules = get_default_reviewers_data(
848 825 self._rhodecode_db_user, source_db_repo,
849 826 source_commit, target_db_repo, target_commit)
850 827
851 828 given_reviewers = _form['review_members']
852 829 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
853 830
854 831 try:
855 832 pull_request = PullRequestModel().create(
856 833 self._rhodecode_user.user_id, source_repo, source_ref,
857 834 target_repo, target_ref, commit_ids, reviewers,
858 835 pullrequest_title, description, reviewer_rules
859 836 )
860 837 Session().commit()
861 838
862 839 h.flash(_('Successfully opened new pull request'),
863 840 category='success')
864 841 except Exception:
865 842 msg = _('Error occurred during creation of this pull request.')
866 843 log.exception(msg)
867 844 h.flash(msg, category='error')
868 845
869 846 # copy the args back to redirect
870 847 org_query = self.request.GET.mixed()
871 848 raise HTTPFound(
872 849 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
873 850 _query=org_query))
874 851
875 852 raise HTTPFound(
876 853 h.route_path('pullrequest_show', repo_name=target_repo,
877 854 pull_request_id=pull_request.pull_request_id))
878 855
879 856 @LoginRequired()
880 857 @NotAnonymous()
881 858 @HasRepoPermissionAnyDecorator(
882 859 'repository.read', 'repository.write', 'repository.admin')
883 860 @CSRFRequired()
884 861 @view_config(
885 862 route_name='pullrequest_update', request_method='POST',
886 863 renderer='json_ext')
887 864 def pull_request_update(self):
888 865 pull_request = PullRequest.get_or_404(
889 866 self.request.matchdict['pull_request_id'])
890 867 _ = self.request.translate
891 868
892 869 self.load_default_context()
893 870
894 871 if pull_request.is_closed():
895 872 log.debug('update: forbidden because pull request is closed')
896 873 msg = _(u'Cannot update closed pull requests.')
897 874 h.flash(msg, category='error')
898 875 return True
899 876
900 877 # only owner or admin can update it
901 878 allowed_to_update = PullRequestModel().check_user_update(
902 879 pull_request, self._rhodecode_user)
903 880 if allowed_to_update:
904 881 controls = peppercorn.parse(self.request.POST.items())
905 882
906 883 if 'review_members' in controls:
907 884 self._update_reviewers(
908 885 pull_request, controls['review_members'],
909 886 pull_request.reviewer_data)
910 887 elif str2bool(self.request.POST.get('update_commits', 'false')):
911 888 self._update_commits(pull_request)
912 889 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
913 890 self._edit_pull_request(pull_request)
914 891 else:
915 892 raise HTTPBadRequest()
916 893 return True
917 894 raise HTTPForbidden()
918 895
919 896 def _edit_pull_request(self, pull_request):
920 897 _ = self.request.translate
921 898 try:
922 899 PullRequestModel().edit(
923 900 pull_request, self.request.POST.get('title'),
924 901 self.request.POST.get('description'), self._rhodecode_user)
925 902 except ValueError:
926 903 msg = _(u'Cannot update closed pull requests.')
927 904 h.flash(msg, category='error')
928 905 return
929 906 else:
930 907 Session().commit()
931 908
932 909 msg = _(u'Pull request title & description updated.')
933 910 h.flash(msg, category='success')
934 911 return
935 912
936 913 def _update_commits(self, pull_request):
937 914 _ = self.request.translate
938 915 resp = PullRequestModel().update_commits(pull_request)
939 916
940 917 if resp.executed:
941 918
942 919 if resp.target_changed and resp.source_changed:
943 920 changed = 'target and source repositories'
944 921 elif resp.target_changed and not resp.source_changed:
945 922 changed = 'target repository'
946 923 elif not resp.target_changed and resp.source_changed:
947 924 changed = 'source repository'
948 925 else:
949 926 changed = 'nothing'
950 927
951 928 msg = _(
952 929 u'Pull request updated to "{source_commit_id}" with '
953 930 u'{count_added} added, {count_removed} removed commits. '
954 931 u'Source of changes: {change_source}')
955 932 msg = msg.format(
956 933 source_commit_id=pull_request.source_ref_parts.commit_id,
957 934 count_added=len(resp.changes.added),
958 935 count_removed=len(resp.changes.removed),
959 936 change_source=changed)
960 937 h.flash(msg, category='success')
961 938
962 939 channel = '/repo${}$/pr/{}'.format(
963 940 pull_request.target_repo.repo_name,
964 941 pull_request.pull_request_id)
965 942 message = msg + (
966 943 ' - <a onclick="window.location.reload()">'
967 944 '<strong>{}</strong></a>'.format(_('Reload page')))
968 945 channelstream.post_message(
969 946 channel, message, self._rhodecode_user.username,
970 947 registry=self.request.registry)
971 948 else:
972 949 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
973 950 warning_reasons = [
974 951 UpdateFailureReason.NO_CHANGE,
975 952 UpdateFailureReason.WRONG_REF_TYPE,
976 953 ]
977 954 category = 'warning' if resp.reason in warning_reasons else 'error'
978 955 h.flash(msg, category=category)
979 956
980 957 @LoginRequired()
981 958 @NotAnonymous()
982 959 @HasRepoPermissionAnyDecorator(
983 960 'repository.read', 'repository.write', 'repository.admin')
984 961 @CSRFRequired()
985 962 @view_config(
986 963 route_name='pullrequest_merge', request_method='POST',
987 964 renderer='json_ext')
988 965 def pull_request_merge(self):
989 966 """
990 967 Merge will perform a server-side merge of the specified
991 968 pull request, if the pull request is approved and mergeable.
992 969 After successful merging, the pull request is automatically
993 970 closed, with a relevant comment.
994 971 """
995 972 pull_request = PullRequest.get_or_404(
996 973 self.request.matchdict['pull_request_id'])
997 974
998 975 self.load_default_context()
999 976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1000 977 translator=self.request.translate)
1001 978 merge_possible = not check.failed
1002 979
1003 980 for err_type, error_msg in check.errors:
1004 981 h.flash(error_msg, category=err_type)
1005 982
1006 983 if merge_possible:
1007 984 log.debug("Pre-conditions checked, trying to merge.")
1008 985 extras = vcs_operation_context(
1009 986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1010 987 username=self._rhodecode_db_user.username, action='push',
1011 988 scm=pull_request.target_repo.repo_type)
1012 989 self._merge_pull_request(
1013 990 pull_request, self._rhodecode_db_user, extras)
1014 991 else:
1015 992 log.debug("Pre-conditions failed, NOT merging.")
1016 993
1017 994 raise HTTPFound(
1018 995 h.route_path('pullrequest_show',
1019 996 repo_name=pull_request.target_repo.repo_name,
1020 997 pull_request_id=pull_request.pull_request_id))
1021 998
1022 999 def _merge_pull_request(self, pull_request, user, extras):
1023 1000 _ = self.request.translate
1024 1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1025 1002
1026 1003 if merge_resp.executed:
1027 1004 log.debug("The merge was successful, closing the pull request.")
1028 1005 PullRequestModel().close_pull_request(
1029 1006 pull_request.pull_request_id, user)
1030 1007 Session().commit()
1031 1008 msg = _('Pull request was successfully merged and closed.')
1032 1009 h.flash(msg, category='success')
1033 1010 else:
1034 1011 log.debug(
1035 1012 "The merge was not successful. Merge response: %s",
1036 1013 merge_resp)
1037 1014 msg = PullRequestModel().merge_status_message(
1038 1015 merge_resp.failure_reason)
1039 1016 h.flash(msg, category='error')
1040 1017
1041 1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1042 1019 _ = self.request.translate
1043 1020 get_default_reviewers_data, validate_default_reviewers = \
1044 1021 PullRequestModel().get_reviewer_functions()
1045 1022
1046 1023 try:
1047 1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1048 1025 except ValueError as e:
1049 1026 log.error('Reviewers Validation: {}'.format(e))
1050 1027 h.flash(e, category='error')
1051 1028 return
1052 1029
1053 1030 PullRequestModel().update_reviewers(
1054 1031 pull_request, reviewers, self._rhodecode_user)
1055 1032 h.flash(_('Pull request reviewers updated.'), category='success')
1056 1033 Session().commit()
1057 1034
1058 1035 @LoginRequired()
1059 1036 @NotAnonymous()
1060 1037 @HasRepoPermissionAnyDecorator(
1061 1038 'repository.read', 'repository.write', 'repository.admin')
1062 1039 @CSRFRequired()
1063 1040 @view_config(
1064 1041 route_name='pullrequest_delete', request_method='POST',
1065 1042 renderer='json_ext')
1066 1043 def pull_request_delete(self):
1067 1044 _ = self.request.translate
1068 1045
1069 1046 pull_request = PullRequest.get_or_404(
1070 1047 self.request.matchdict['pull_request_id'])
1071 1048 self.load_default_context()
1072 1049
1073 1050 pr_closed = pull_request.is_closed()
1074 1051 allowed_to_delete = PullRequestModel().check_user_delete(
1075 1052 pull_request, self._rhodecode_user) and not pr_closed
1076 1053
1077 1054 # only owner can delete it !
1078 1055 if allowed_to_delete:
1079 1056 PullRequestModel().delete(pull_request, self._rhodecode_user)
1080 1057 Session().commit()
1081 1058 h.flash(_('Successfully deleted pull request'),
1082 1059 category='success')
1083 1060 raise HTTPFound(h.route_path('pullrequest_show_all',
1084 1061 repo_name=self.db_repo_name))
1085 1062
1086 1063 log.warning('user %s tried to delete pull request without access',
1087 1064 self._rhodecode_user)
1088 1065 raise HTTPNotFound()
1089 1066
1090 1067 @LoginRequired()
1091 1068 @NotAnonymous()
1092 1069 @HasRepoPermissionAnyDecorator(
1093 1070 'repository.read', 'repository.write', 'repository.admin')
1094 1071 @CSRFRequired()
1095 1072 @view_config(
1096 1073 route_name='pullrequest_comment_create', request_method='POST',
1097 1074 renderer='json_ext')
1098 1075 def pull_request_comment_create(self):
1099 1076 _ = self.request.translate
1100 1077
1101 1078 pull_request = PullRequest.get_or_404(
1102 1079 self.request.matchdict['pull_request_id'])
1103 1080 pull_request_id = pull_request.pull_request_id
1104 1081
1105 1082 if pull_request.is_closed():
1106 1083 log.debug('comment: forbidden because pull request is closed')
1107 1084 raise HTTPForbidden()
1108 1085
1109 1086 allowed_to_comment = PullRequestModel().check_user_comment(
1110 1087 pull_request, self._rhodecode_user)
1111 1088 if not allowed_to_comment:
1112 1089 log.debug(
1113 1090 'comment: forbidden because pull request is from forbidden repo')
1114 1091 raise HTTPForbidden()
1115 1092
1116 1093 c = self.load_default_context()
1117 1094
1118 1095 status = self.request.POST.get('changeset_status', None)
1119 1096 text = self.request.POST.get('text')
1120 1097 comment_type = self.request.POST.get('comment_type')
1121 1098 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1122 1099 close_pull_request = self.request.POST.get('close_pull_request')
1123 1100
1124 1101 # the logic here should work like following, if we submit close
1125 1102 # pr comment, use `close_pull_request_with_comment` function
1126 1103 # else handle regular comment logic
1127 1104
1128 1105 if close_pull_request:
1129 1106 # only owner or admin or person with write permissions
1130 1107 allowed_to_close = PullRequestModel().check_user_update(
1131 1108 pull_request, self._rhodecode_user)
1132 1109 if not allowed_to_close:
1133 1110 log.debug('comment: forbidden because not allowed to close '
1134 1111 'pull request %s', pull_request_id)
1135 1112 raise HTTPForbidden()
1136 1113 comment, status = PullRequestModel().close_pull_request_with_comment(
1137 1114 pull_request, self._rhodecode_user, self.db_repo, message=text)
1138 1115 Session().flush()
1139 1116 events.trigger(
1140 1117 events.PullRequestCommentEvent(pull_request, comment))
1141 1118
1142 1119 else:
1143 1120 # regular comment case, could be inline, or one with status.
1144 1121 # for that one we check also permissions
1145 1122
1146 1123 allowed_to_change_status = PullRequestModel().check_user_change_status(
1147 1124 pull_request, self._rhodecode_user)
1148 1125
1149 1126 if status and allowed_to_change_status:
1150 1127 message = (_('Status change %(transition_icon)s %(status)s')
1151 1128 % {'transition_icon': '>',
1152 1129 'status': ChangesetStatus.get_status_lbl(status)})
1153 1130 text = text or message
1154 1131
1155 1132 comment = CommentsModel().create(
1156 1133 text=text,
1157 1134 repo=self.db_repo.repo_id,
1158 1135 user=self._rhodecode_user.user_id,
1159 1136 pull_request=pull_request,
1160 1137 f_path=self.request.POST.get('f_path'),
1161 1138 line_no=self.request.POST.get('line'),
1162 1139 status_change=(ChangesetStatus.get_status_lbl(status)
1163 1140 if status and allowed_to_change_status else None),
1164 1141 status_change_type=(status
1165 1142 if status and allowed_to_change_status else None),
1166 1143 comment_type=comment_type,
1167 1144 resolves_comment_id=resolves_comment_id
1168 1145 )
1169 1146
1170 1147 if allowed_to_change_status:
1171 1148 # calculate old status before we change it
1172 1149 old_calculated_status = pull_request.calculated_review_status()
1173 1150
1174 1151 # get status if set !
1175 1152 if status:
1176 1153 ChangesetStatusModel().set_status(
1177 1154 self.db_repo.repo_id,
1178 1155 status,
1179 1156 self._rhodecode_user.user_id,
1180 1157 comment,
1181 1158 pull_request=pull_request
1182 1159 )
1183 1160
1184 1161 Session().flush()
1185 1162 events.trigger(
1186 1163 events.PullRequestCommentEvent(pull_request, comment))
1187 1164
1188 1165 # we now calculate the status of pull request, and based on that
1189 1166 # calculation we set the commits status
1190 1167 calculated_status = pull_request.calculated_review_status()
1191 1168 if old_calculated_status != calculated_status:
1192 1169 PullRequestModel()._trigger_pull_request_hook(
1193 1170 pull_request, self._rhodecode_user, 'review_status_change')
1194 1171
1195 1172 Session().commit()
1196 1173
1197 1174 data = {
1198 1175 'target_id': h.safeid(h.safe_unicode(
1199 1176 self.request.POST.get('f_path'))),
1200 1177 }
1201 1178 if comment:
1202 1179 c.co = comment
1203 1180 rendered_comment = render(
1204 1181 'rhodecode:templates/changeset/changeset_comment_block.mako',
1205 1182 self._get_template_context(c), self.request)
1206 1183
1207 1184 data.update(comment.get_dict())
1208 1185 data.update({'rendered_text': rendered_comment})
1209 1186
1210 1187 return data
1211 1188
1212 1189 @LoginRequired()
1213 1190 @NotAnonymous()
1214 1191 @HasRepoPermissionAnyDecorator(
1215 1192 'repository.read', 'repository.write', 'repository.admin')
1216 1193 @CSRFRequired()
1217 1194 @view_config(
1218 1195 route_name='pullrequest_comment_delete', request_method='POST',
1219 1196 renderer='json_ext')
1220 1197 def pull_request_comment_delete(self):
1221 1198 pull_request = PullRequest.get_or_404(
1222 1199 self.request.matchdict['pull_request_id'])
1223 1200
1224 1201 comment = ChangesetComment.get_or_404(
1225 1202 self.request.matchdict['comment_id'])
1226 1203 comment_id = comment.comment_id
1227 1204
1228 1205 if pull_request.is_closed():
1229 1206 log.debug('comment: forbidden because pull request is closed')
1230 1207 raise HTTPForbidden()
1231 1208
1232 1209 if not comment:
1233 1210 log.debug('Comment with id:%s not found, skipping', comment_id)
1234 1211 # comment already deleted in another call probably
1235 1212 return True
1236 1213
1237 1214 if comment.pull_request.is_closed():
1238 1215 # don't allow deleting comments on closed pull request
1239 1216 raise HTTPForbidden()
1240 1217
1241 1218 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1242 1219 super_admin = h.HasPermissionAny('hg.admin')()
1243 1220 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1244 1221 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1245 1222 comment_repo_admin = is_repo_admin and is_repo_comment
1246 1223
1247 1224 if super_admin or comment_owner or comment_repo_admin:
1248 1225 old_calculated_status = comment.pull_request.calculated_review_status()
1249 1226 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1250 1227 Session().commit()
1251 1228 calculated_status = comment.pull_request.calculated_review_status()
1252 1229 if old_calculated_status != calculated_status:
1253 1230 PullRequestModel()._trigger_pull_request_hook(
1254 1231 comment.pull_request, self._rhodecode_user, 'review_status_change')
1255 1232 return True
1256 1233 else:
1257 1234 log.warning('No permissions for user %s to delete comment_id: %s',
1258 1235 self._rhodecode_db_user, comment_id)
1259 1236 raise HTTPNotFound()
@@ -1,1628 +1,1651 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31 import collections
32 32
33 33 from pyramid.threadlocal import get_current_request
34 34
35 35 from rhodecode import events
36 36 from rhodecode.translation import lazy_ugettext#, _
37 37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 41 from rhodecode.lib.markup_renderer import (
42 42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 44 from rhodecode.lib.vcs.backends.base import (
45 45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 47 from rhodecode.lib.vcs.exceptions import (
48 48 CommitDoesNotExistError, EmptyRepositoryError)
49 49 from rhodecode.model import BaseModel
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.db import (
53 53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 54 PullRequestVersion, ChangesetComment, Repository)
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.notification import NotificationModel, \
57 57 EmailNotificationModel
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.settings import VcsSettingsModel
60 60
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 # Data structure to hold the response data when updating commits during a pull
66 66 # request update.
67 67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 68 'executed', 'reason', 'new', 'old', 'changes',
69 69 'source_changed', 'target_changed'])
70 70
71 71
72 72 class PullRequestModel(BaseModel):
73 73
74 74 cls = PullRequest
75 75
76 76 DIFF_CONTEXT = 3
77 77
78 78 MERGE_STATUS_MESSAGES = {
79 79 MergeFailureReason.NONE: lazy_ugettext(
80 80 'This pull request can be automatically merged.'),
81 81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 82 'This pull request cannot be merged because of an unhandled'
83 83 ' exception.'),
84 84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 85 'This pull request cannot be merged because of merge conflicts.'),
86 86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 87 'This pull request could not be merged because push to target'
88 88 ' failed.'),
89 89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 90 'This pull request cannot be merged because the target is not a'
91 91 ' head.'),
92 92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 93 'This pull request cannot be merged because the source contains'
94 94 ' more branches than the target.'),
95 95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 96 'This pull request cannot be merged because the target has'
97 97 ' multiple heads.'),
98 98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 99 'This pull request cannot be merged because the target repository'
100 100 ' is locked.'),
101 101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 102 'This pull request cannot be merged because the target or the '
103 103 'source reference is missing.'),
104 104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 105 'This pull request cannot be merged because the target '
106 106 'reference is missing.'),
107 107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 108 'This pull request cannot be merged because the source '
109 109 'reference is missing.'),
110 110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 111 'This pull request cannot be merged because of conflicts related '
112 112 'to sub repositories.'),
113 113 }
114 114
115 115 UPDATE_STATUS_MESSAGES = {
116 116 UpdateFailureReason.NONE: lazy_ugettext(
117 117 'Pull request update successful.'),
118 118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 119 'Pull request update failed because of an unknown error.'),
120 120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 121 'No update needed because the source and target have not changed.'),
122 122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 123 'Pull request cannot be updated because the reference type is '
124 124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 126 'This pull request cannot be updated because the target '
127 127 'reference is missing.'),
128 128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 129 'This pull request cannot be updated because the source '
130 130 'reference is missing.'),
131 131 }
132 132
133 133 def __get_pull_request(self, pull_request):
134 134 return self._get_instance((
135 135 PullRequest, PullRequestVersion), pull_request)
136 136
137 137 def _check_perms(self, perms, pull_request, user, api=False):
138 138 if not api:
139 139 return h.HasRepoPermissionAny(*perms)(
140 140 user=user, repo_name=pull_request.target_repo.repo_name)
141 141 else:
142 142 return h.HasRepoPermissionAnyApi(*perms)(
143 143 user=user, repo_name=pull_request.target_repo.repo_name)
144 144
145 145 def check_user_read(self, pull_request, user, api=False):
146 146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 147 return self._check_perms(_perms, pull_request, user, api)
148 148
149 149 def check_user_merge(self, pull_request, user, api=False):
150 150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 151 return self._check_perms(_perms, pull_request, user, api)
152 152
153 153 def check_user_update(self, pull_request, user, api=False):
154 154 owner = user.user_id == pull_request.user_id
155 155 return self.check_user_merge(pull_request, user, api) or owner
156 156
157 157 def check_user_delete(self, pull_request, user):
158 158 owner = user.user_id == pull_request.user_id
159 159 _perms = ('repository.admin',)
160 160 return self._check_perms(_perms, pull_request, user) or owner
161 161
162 162 def check_user_change_status(self, pull_request, user, api=False):
163 163 reviewer = user.user_id in [x.user_id for x in
164 164 pull_request.reviewers]
165 165 return self.check_user_update(pull_request, user, api) or reviewer
166 166
167 167 def check_user_comment(self, pull_request, user):
168 168 owner = user.user_id == pull_request.user_id
169 169 return self.check_user_read(pull_request, user) or owner
170 170
171 171 def get(self, pull_request):
172 172 return self.__get_pull_request(pull_request)
173 173
174 174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 175 opened_by=None, order_by=None,
176 176 order_dir='desc'):
177 177 repo = None
178 178 if repo_name:
179 179 repo = self._get_repo(repo_name)
180 180
181 181 q = PullRequest.query()
182 182
183 183 # source or target
184 184 if repo and source:
185 185 q = q.filter(PullRequest.source_repo == repo)
186 186 elif repo:
187 187 q = q.filter(PullRequest.target_repo == repo)
188 188
189 189 # closed,opened
190 190 if statuses:
191 191 q = q.filter(PullRequest.status.in_(statuses))
192 192
193 193 # opened by filter
194 194 if opened_by:
195 195 q = q.filter(PullRequest.user_id.in_(opened_by))
196 196
197 197 if order_by:
198 198 order_map = {
199 199 'name_raw': PullRequest.pull_request_id,
200 200 'title': PullRequest.title,
201 201 'updated_on_raw': PullRequest.updated_on,
202 202 'target_repo': PullRequest.target_repo_id
203 203 }
204 204 if order_dir == 'asc':
205 205 q = q.order_by(order_map[order_by].asc())
206 206 else:
207 207 q = q.order_by(order_map[order_by].desc())
208 208
209 209 return q
210 210
211 211 def count_all(self, repo_name, source=False, statuses=None,
212 212 opened_by=None):
213 213 """
214 214 Count the number of pull requests for a specific repository.
215 215
216 216 :param repo_name: target or source repo
217 217 :param source: boolean flag to specify if repo_name refers to source
218 218 :param statuses: list of pull request statuses
219 219 :param opened_by: author user of the pull request
220 220 :returns: int number of pull requests
221 221 """
222 222 q = self._prepare_get_all_query(
223 223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224 224
225 225 return q.count()
226 226
227 227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 228 offset=0, length=None, order_by=None, order_dir='desc'):
229 229 """
230 230 Get all pull requests for a specific repository.
231 231
232 232 :param repo_name: target or source repo
233 233 :param source: boolean flag to specify if repo_name refers to source
234 234 :param statuses: list of pull request statuses
235 235 :param opened_by: author user of the pull request
236 236 :param offset: pagination offset
237 237 :param length: length of returned list
238 238 :param order_by: order of the returned list
239 239 :param order_dir: 'asc' or 'desc' ordering direction
240 240 :returns: list of pull requests
241 241 """
242 242 q = self._prepare_get_all_query(
243 243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 244 order_by=order_by, order_dir=order_dir)
245 245
246 246 if length:
247 247 pull_requests = q.limit(length).offset(offset).all()
248 248 else:
249 249 pull_requests = q.all()
250 250
251 251 return pull_requests
252 252
253 253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 254 opened_by=None):
255 255 """
256 256 Count the number of pull requests for a specific repository that are
257 257 awaiting review.
258 258
259 259 :param repo_name: target or source repo
260 260 :param source: boolean flag to specify if repo_name refers to source
261 261 :param statuses: list of pull request statuses
262 262 :param opened_by: author user of the pull request
263 263 :returns: int number of pull requests
264 264 """
265 265 pull_requests = self.get_awaiting_review(
266 266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267 267
268 268 return len(pull_requests)
269 269
270 270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 271 opened_by=None, offset=0, length=None,
272 272 order_by=None, order_dir='desc'):
273 273 """
274 274 Get all pull requests for a specific repository that are awaiting
275 275 review.
276 276
277 277 :param repo_name: target or source repo
278 278 :param source: boolean flag to specify if repo_name refers to source
279 279 :param statuses: list of pull request statuses
280 280 :param opened_by: author user of the pull request
281 281 :param offset: pagination offset
282 282 :param length: length of returned list
283 283 :param order_by: order of the returned list
284 284 :param order_dir: 'asc' or 'desc' ordering direction
285 285 :returns: list of pull requests
286 286 """
287 287 pull_requests = self.get_all(
288 288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 289 order_by=order_by, order_dir=order_dir)
290 290
291 291 _filtered_pull_requests = []
292 292 for pr in pull_requests:
293 293 status = pr.calculated_review_status()
294 294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 296 _filtered_pull_requests.append(pr)
297 297 if length:
298 298 return _filtered_pull_requests[offset:offset+length]
299 299 else:
300 300 return _filtered_pull_requests
301 301
302 302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 303 opened_by=None, user_id=None):
304 304 """
305 305 Count the number of pull requests for a specific repository that are
306 306 awaiting review from a specific user.
307 307
308 308 :param repo_name: target or source repo
309 309 :param source: boolean flag to specify if repo_name refers to source
310 310 :param statuses: list of pull request statuses
311 311 :param opened_by: author user of the pull request
312 312 :param user_id: reviewer user of the pull request
313 313 :returns: int number of pull requests
314 314 """
315 315 pull_requests = self.get_awaiting_my_review(
316 316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 317 user_id=user_id)
318 318
319 319 return len(pull_requests)
320 320
321 321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 322 opened_by=None, user_id=None, offset=0,
323 323 length=None, order_by=None, order_dir='desc'):
324 324 """
325 325 Get all pull requests for a specific repository that are awaiting
326 326 review from a specific user.
327 327
328 328 :param repo_name: target or source repo
329 329 :param source: boolean flag to specify if repo_name refers to source
330 330 :param statuses: list of pull request statuses
331 331 :param opened_by: author user of the pull request
332 332 :param user_id: reviewer user of the pull request
333 333 :param offset: pagination offset
334 334 :param length: length of returned list
335 335 :param order_by: order of the returned list
336 336 :param order_dir: 'asc' or 'desc' ordering direction
337 337 :returns: list of pull requests
338 338 """
339 339 pull_requests = self.get_all(
340 340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 341 order_by=order_by, order_dir=order_dir)
342 342
343 343 _my = PullRequestModel().get_not_reviewed(user_id)
344 344 my_participation = []
345 345 for pr in pull_requests:
346 346 if pr in _my:
347 347 my_participation.append(pr)
348 348 _filtered_pull_requests = my_participation
349 349 if length:
350 350 return _filtered_pull_requests[offset:offset+length]
351 351 else:
352 352 return _filtered_pull_requests
353 353
354 354 def get_not_reviewed(self, user_id):
355 355 return [
356 356 x.pull_request for x in PullRequestReviewers.query().filter(
357 357 PullRequestReviewers.user_id == user_id).all()
358 358 ]
359 359
360 360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 361 order_by=None, order_dir='desc'):
362 362 q = PullRequest.query()
363 363 if user_id:
364 364 reviewers_subquery = Session().query(
365 365 PullRequestReviewers.pull_request_id).filter(
366 366 PullRequestReviewers.user_id == user_id).subquery()
367 367 user_filter = or_(
368 368 PullRequest.user_id == user_id,
369 369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 370 )
371 371 q = PullRequest.query().filter(user_filter)
372 372
373 373 # closed,opened
374 374 if statuses:
375 375 q = q.filter(PullRequest.status.in_(statuses))
376 376
377 377 if order_by:
378 378 order_map = {
379 379 'name_raw': PullRequest.pull_request_id,
380 380 'title': PullRequest.title,
381 381 'updated_on_raw': PullRequest.updated_on,
382 382 'target_repo': PullRequest.target_repo_id
383 383 }
384 384 if order_dir == 'asc':
385 385 q = q.order_by(order_map[order_by].asc())
386 386 else:
387 387 q = q.order_by(order_map[order_by].desc())
388 388
389 389 return q
390 390
391 391 def count_im_participating_in(self, user_id=None, statuses=None):
392 392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 393 return q.count()
394 394
395 395 def get_im_participating_in(
396 396 self, user_id=None, statuses=None, offset=0,
397 397 length=None, order_by=None, order_dir='desc'):
398 398 """
399 399 Get all Pull requests that i'm participating in, or i have opened
400 400 """
401 401
402 402 q = self._prepare_participating_query(
403 403 user_id, statuses=statuses, order_by=order_by,
404 404 order_dir=order_dir)
405 405
406 406 if length:
407 407 pull_requests = q.limit(length).offset(offset).all()
408 408 else:
409 409 pull_requests = q.all()
410 410
411 411 return pull_requests
412 412
413 413 def get_versions(self, pull_request):
414 414 """
415 415 returns version of pull request sorted by ID descending
416 416 """
417 417 return PullRequestVersion.query()\
418 418 .filter(PullRequestVersion.pull_request == pull_request)\
419 419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 420 .all()
421 421
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
424
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
441
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
444
422 445 def create(self, created_by, source_repo, source_ref, target_repo,
423 446 target_ref, revisions, reviewers, title, description=None,
424 447 reviewer_data=None, translator=None):
425 448 translator = translator or get_current_request().translate
426 449
427 450 created_by_user = self._get_user(created_by)
428 451 source_repo = self._get_repo(source_repo)
429 452 target_repo = self._get_repo(target_repo)
430 453
431 454 pull_request = PullRequest()
432 455 pull_request.source_repo = source_repo
433 456 pull_request.source_ref = source_ref
434 457 pull_request.target_repo = target_repo
435 458 pull_request.target_ref = target_ref
436 459 pull_request.revisions = revisions
437 460 pull_request.title = title
438 461 pull_request.description = description
439 462 pull_request.author = created_by_user
440 463 pull_request.reviewer_data = reviewer_data
441 464
442 465 Session().add(pull_request)
443 466 Session().flush()
444 467
445 468 reviewer_ids = set()
446 469 # members / reviewers
447 470 for reviewer_object in reviewers:
448 471 user_id, reasons, mandatory = reviewer_object
449 472 user = self._get_user(user_id)
450 473
451 474 # skip duplicates
452 475 if user.user_id in reviewer_ids:
453 476 continue
454 477
455 478 reviewer_ids.add(user.user_id)
456 479
457 480 reviewer = PullRequestReviewers()
458 481 reviewer.user = user
459 482 reviewer.pull_request = pull_request
460 483 reviewer.reasons = reasons
461 484 reviewer.mandatory = mandatory
462 485 Session().add(reviewer)
463 486
464 487 # Set approval status to "Under Review" for all commits which are
465 488 # part of this pull request.
466 489 ChangesetStatusModel().set_status(
467 490 repo=target_repo,
468 491 status=ChangesetStatus.STATUS_UNDER_REVIEW,
469 492 user=created_by_user,
470 493 pull_request=pull_request
471 494 )
472 495
473 496 MergeCheck.validate(
474 497 pull_request, user=created_by_user, translator=translator)
475 498
476 499 self.notify_reviewers(pull_request, reviewer_ids)
477 500 self._trigger_pull_request_hook(
478 501 pull_request, created_by_user, 'create')
479 502
480 503 creation_data = pull_request.get_api_data(with_merge_state=False)
481 504 self._log_audit_action(
482 505 'repo.pull_request.create', {'data': creation_data},
483 506 created_by_user, pull_request)
484 507
485 508 return pull_request
486 509
487 510 def _trigger_pull_request_hook(self, pull_request, user, action):
488 511 pull_request = self.__get_pull_request(pull_request)
489 512 target_scm = pull_request.target_repo.scm_instance()
490 513 if action == 'create':
491 514 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
492 515 elif action == 'merge':
493 516 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
494 517 elif action == 'close':
495 518 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
496 519 elif action == 'review_status_change':
497 520 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
498 521 elif action == 'update':
499 522 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
500 523 else:
501 524 return
502 525
503 526 trigger_hook(
504 527 username=user.username,
505 528 repo_name=pull_request.target_repo.repo_name,
506 529 repo_alias=target_scm.alias,
507 530 pull_request=pull_request)
508 531
509 532 def _get_commit_ids(self, pull_request):
510 533 """
511 534 Return the commit ids of the merged pull request.
512 535
513 536 This method is not dealing correctly yet with the lack of autoupdates
514 537 nor with the implicit target updates.
515 538 For example: if a commit in the source repo is already in the target it
516 539 will be reported anyways.
517 540 """
518 541 merge_rev = pull_request.merge_rev
519 542 if merge_rev is None:
520 543 raise ValueError('This pull request was not merged yet')
521 544
522 545 commit_ids = list(pull_request.revisions)
523 546 if merge_rev not in commit_ids:
524 547 commit_ids.append(merge_rev)
525 548
526 549 return commit_ids
527 550
528 551 def merge(self, pull_request, user, extras):
529 552 log.debug("Merging pull request %s", pull_request.pull_request_id)
530 553 merge_state = self._merge_pull_request(pull_request, user, extras)
531 554 if merge_state.executed:
532 555 log.debug(
533 556 "Merge was successful, updating the pull request comments.")
534 557 self._comment_and_close_pr(pull_request, user, merge_state)
535 558
536 559 self._log_audit_action(
537 560 'repo.pull_request.merge',
538 561 {'merge_state': merge_state.__dict__},
539 562 user, pull_request)
540 563
541 564 else:
542 565 log.warn("Merge failed, not updating the pull request.")
543 566 return merge_state
544 567
545 568 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
546 569 target_vcs = pull_request.target_repo.scm_instance()
547 570 source_vcs = pull_request.source_repo.scm_instance()
548 571 target_ref = self._refresh_reference(
549 572 pull_request.target_ref_parts, target_vcs)
550 573
551 574 message = merge_msg or (
552 575 'Merge pull request #%(pr_id)s from '
553 576 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
554 577 'pr_id': pull_request.pull_request_id,
555 578 'source_repo': source_vcs.name,
556 579 'source_ref_name': pull_request.source_ref_parts.name,
557 580 'pr_title': pull_request.title
558 581 }
559 582
560 583 workspace_id = self._workspace_id(pull_request)
561 584 use_rebase = self._use_rebase_for_merging(pull_request)
562 585 close_branch = self._close_branch_before_merging(pull_request)
563 586
564 587 callback_daemon, extras = prepare_callback_daemon(
565 588 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
566 589 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
567 590
568 591 with callback_daemon:
569 592 # TODO: johbo: Implement a clean way to run a config_override
570 593 # for a single call.
571 594 target_vcs.config.set(
572 595 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
573 596 merge_state = target_vcs.merge(
574 597 target_ref, source_vcs, pull_request.source_ref_parts,
575 598 workspace_id, user_name=user.username,
576 599 user_email=user.email, message=message, use_rebase=use_rebase,
577 600 close_branch=close_branch)
578 601 return merge_state
579 602
580 603 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
581 604 pull_request.merge_rev = merge_state.merge_ref.commit_id
582 605 pull_request.updated_on = datetime.datetime.now()
583 606 close_msg = close_msg or 'Pull request merged and closed'
584 607
585 608 CommentsModel().create(
586 609 text=safe_unicode(close_msg),
587 610 repo=pull_request.target_repo.repo_id,
588 611 user=user.user_id,
589 612 pull_request=pull_request.pull_request_id,
590 613 f_path=None,
591 614 line_no=None,
592 615 closing_pr=True
593 616 )
594 617
595 618 Session().add(pull_request)
596 619 Session().flush()
597 620 # TODO: paris: replace invalidation with less radical solution
598 621 ScmModel().mark_for_invalidation(
599 622 pull_request.target_repo.repo_name)
600 623 self._trigger_pull_request_hook(pull_request, user, 'merge')
601 624
602 625 def has_valid_update_type(self, pull_request):
603 626 source_ref_type = pull_request.source_ref_parts.type
604 627 return source_ref_type in ['book', 'branch', 'tag']
605 628
606 629 def update_commits(self, pull_request):
607 630 """
608 631 Get the updated list of commits for the pull request
609 632 and return the new pull request version and the list
610 633 of commits processed by this update action
611 634 """
612 635 pull_request = self.__get_pull_request(pull_request)
613 636 source_ref_type = pull_request.source_ref_parts.type
614 637 source_ref_name = pull_request.source_ref_parts.name
615 638 source_ref_id = pull_request.source_ref_parts.commit_id
616 639
617 640 target_ref_type = pull_request.target_ref_parts.type
618 641 target_ref_name = pull_request.target_ref_parts.name
619 642 target_ref_id = pull_request.target_ref_parts.commit_id
620 643
621 644 if not self.has_valid_update_type(pull_request):
622 645 log.debug(
623 646 "Skipping update of pull request %s due to ref type: %s",
624 647 pull_request, source_ref_type)
625 648 return UpdateResponse(
626 649 executed=False,
627 650 reason=UpdateFailureReason.WRONG_REF_TYPE,
628 651 old=pull_request, new=None, changes=None,
629 652 source_changed=False, target_changed=False)
630 653
631 654 # source repo
632 655 source_repo = pull_request.source_repo.scm_instance()
633 656 try:
634 657 source_commit = source_repo.get_commit(commit_id=source_ref_name)
635 658 except CommitDoesNotExistError:
636 659 return UpdateResponse(
637 660 executed=False,
638 661 reason=UpdateFailureReason.MISSING_SOURCE_REF,
639 662 old=pull_request, new=None, changes=None,
640 663 source_changed=False, target_changed=False)
641 664
642 665 source_changed = source_ref_id != source_commit.raw_id
643 666
644 667 # target repo
645 668 target_repo = pull_request.target_repo.scm_instance()
646 669 try:
647 670 target_commit = target_repo.get_commit(commit_id=target_ref_name)
648 671 except CommitDoesNotExistError:
649 672 return UpdateResponse(
650 673 executed=False,
651 674 reason=UpdateFailureReason.MISSING_TARGET_REF,
652 675 old=pull_request, new=None, changes=None,
653 676 source_changed=False, target_changed=False)
654 677 target_changed = target_ref_id != target_commit.raw_id
655 678
656 679 if not (source_changed or target_changed):
657 680 log.debug("Nothing changed in pull request %s", pull_request)
658 681 return UpdateResponse(
659 682 executed=False,
660 683 reason=UpdateFailureReason.NO_CHANGE,
661 684 old=pull_request, new=None, changes=None,
662 685 source_changed=target_changed, target_changed=source_changed)
663 686
664 687 change_in_found = 'target repo' if target_changed else 'source repo'
665 688 log.debug('Updating pull request because of change in %s detected',
666 689 change_in_found)
667 690
668 691 # Finally there is a need for an update, in case of source change
669 692 # we create a new version, else just an update
670 693 if source_changed:
671 694 pull_request_version = self._create_version_from_snapshot(pull_request)
672 695 self._link_comments_to_version(pull_request_version)
673 696 else:
674 697 try:
675 698 ver = pull_request.versions[-1]
676 699 except IndexError:
677 700 ver = None
678 701
679 702 pull_request.pull_request_version_id = \
680 703 ver.pull_request_version_id if ver else None
681 704 pull_request_version = pull_request
682 705
683 706 try:
684 707 if target_ref_type in ('tag', 'branch', 'book'):
685 708 target_commit = target_repo.get_commit(target_ref_name)
686 709 else:
687 710 target_commit = target_repo.get_commit(target_ref_id)
688 711 except CommitDoesNotExistError:
689 712 return UpdateResponse(
690 713 executed=False,
691 714 reason=UpdateFailureReason.MISSING_TARGET_REF,
692 715 old=pull_request, new=None, changes=None,
693 716 source_changed=source_changed, target_changed=target_changed)
694 717
695 718 # re-compute commit ids
696 719 old_commit_ids = pull_request.revisions
697 720 pre_load = ["author", "branch", "date", "message"]
698 721 commit_ranges = target_repo.compare(
699 722 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
700 723 pre_load=pre_load)
701 724
702 725 ancestor = target_repo.get_common_ancestor(
703 726 target_commit.raw_id, source_commit.raw_id, source_repo)
704 727
705 728 pull_request.source_ref = '%s:%s:%s' % (
706 729 source_ref_type, source_ref_name, source_commit.raw_id)
707 730 pull_request.target_ref = '%s:%s:%s' % (
708 731 target_ref_type, target_ref_name, ancestor)
709 732
710 733 pull_request.revisions = [
711 734 commit.raw_id for commit in reversed(commit_ranges)]
712 735 pull_request.updated_on = datetime.datetime.now()
713 736 Session().add(pull_request)
714 737 new_commit_ids = pull_request.revisions
715 738
716 739 old_diff_data, new_diff_data = self._generate_update_diffs(
717 740 pull_request, pull_request_version)
718 741
719 742 # calculate commit and file changes
720 743 changes = self._calculate_commit_id_changes(
721 744 old_commit_ids, new_commit_ids)
722 745 file_changes = self._calculate_file_changes(
723 746 old_diff_data, new_diff_data)
724 747
725 748 # set comments as outdated if DIFFS changed
726 749 CommentsModel().outdate_comments(
727 750 pull_request, old_diff_data=old_diff_data,
728 751 new_diff_data=new_diff_data)
729 752
730 753 commit_changes = (changes.added or changes.removed)
731 754 file_node_changes = (
732 755 file_changes.added or file_changes.modified or file_changes.removed)
733 756 pr_has_changes = commit_changes or file_node_changes
734 757
735 758 # Add an automatic comment to the pull request, in case
736 759 # anything has changed
737 760 if pr_has_changes:
738 761 update_comment = CommentsModel().create(
739 762 text=self._render_update_message(changes, file_changes),
740 763 repo=pull_request.target_repo,
741 764 user=pull_request.author,
742 765 pull_request=pull_request,
743 766 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
744 767
745 768 # Update status to "Under Review" for added commits
746 769 for commit_id in changes.added:
747 770 ChangesetStatusModel().set_status(
748 771 repo=pull_request.source_repo,
749 772 status=ChangesetStatus.STATUS_UNDER_REVIEW,
750 773 comment=update_comment,
751 774 user=pull_request.author,
752 775 pull_request=pull_request,
753 776 revision=commit_id)
754 777
755 778 log.debug(
756 779 'Updated pull request %s, added_ids: %s, common_ids: %s, '
757 780 'removed_ids: %s', pull_request.pull_request_id,
758 781 changes.added, changes.common, changes.removed)
759 782 log.debug(
760 783 'Updated pull request with the following file changes: %s',
761 784 file_changes)
762 785
763 786 log.info(
764 787 "Updated pull request %s from commit %s to commit %s, "
765 788 "stored new version %s of this pull request.",
766 789 pull_request.pull_request_id, source_ref_id,
767 790 pull_request.source_ref_parts.commit_id,
768 791 pull_request_version.pull_request_version_id)
769 792 Session().commit()
770 793 self._trigger_pull_request_hook(
771 794 pull_request, pull_request.author, 'update')
772 795
773 796 return UpdateResponse(
774 797 executed=True, reason=UpdateFailureReason.NONE,
775 798 old=pull_request, new=pull_request_version, changes=changes,
776 799 source_changed=source_changed, target_changed=target_changed)
777 800
778 801 def _create_version_from_snapshot(self, pull_request):
779 802 version = PullRequestVersion()
780 803 version.title = pull_request.title
781 804 version.description = pull_request.description
782 805 version.status = pull_request.status
783 806 version.created_on = datetime.datetime.now()
784 807 version.updated_on = pull_request.updated_on
785 808 version.user_id = pull_request.user_id
786 809 version.source_repo = pull_request.source_repo
787 810 version.source_ref = pull_request.source_ref
788 811 version.target_repo = pull_request.target_repo
789 812 version.target_ref = pull_request.target_ref
790 813
791 814 version._last_merge_source_rev = pull_request._last_merge_source_rev
792 815 version._last_merge_target_rev = pull_request._last_merge_target_rev
793 816 version.last_merge_status = pull_request.last_merge_status
794 817 version.shadow_merge_ref = pull_request.shadow_merge_ref
795 818 version.merge_rev = pull_request.merge_rev
796 819 version.reviewer_data = pull_request.reviewer_data
797 820
798 821 version.revisions = pull_request.revisions
799 822 version.pull_request = pull_request
800 823 Session().add(version)
801 824 Session().flush()
802 825
803 826 return version
804 827
805 828 def _generate_update_diffs(self, pull_request, pull_request_version):
806 829
807 830 diff_context = (
808 831 self.DIFF_CONTEXT +
809 832 CommentsModel.needed_extra_diff_context())
810 833
811 834 source_repo = pull_request_version.source_repo
812 835 source_ref_id = pull_request_version.source_ref_parts.commit_id
813 836 target_ref_id = pull_request_version.target_ref_parts.commit_id
814 837 old_diff = self._get_diff_from_pr_or_version(
815 838 source_repo, source_ref_id, target_ref_id, context=diff_context)
816 839
817 840 source_repo = pull_request.source_repo
818 841 source_ref_id = pull_request.source_ref_parts.commit_id
819 842 target_ref_id = pull_request.target_ref_parts.commit_id
820 843
821 844 new_diff = self._get_diff_from_pr_or_version(
822 845 source_repo, source_ref_id, target_ref_id, context=diff_context)
823 846
824 847 old_diff_data = diffs.DiffProcessor(old_diff)
825 848 old_diff_data.prepare()
826 849 new_diff_data = diffs.DiffProcessor(new_diff)
827 850 new_diff_data.prepare()
828 851
829 852 return old_diff_data, new_diff_data
830 853
831 854 def _link_comments_to_version(self, pull_request_version):
832 855 """
833 856 Link all unlinked comments of this pull request to the given version.
834 857
835 858 :param pull_request_version: The `PullRequestVersion` to which
836 859 the comments shall be linked.
837 860
838 861 """
839 862 pull_request = pull_request_version.pull_request
840 863 comments = ChangesetComment.query()\
841 864 .filter(
842 865 # TODO: johbo: Should we query for the repo at all here?
843 866 # Pending decision on how comments of PRs are to be related
844 867 # to either the source repo, the target repo or no repo at all.
845 868 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
846 869 ChangesetComment.pull_request == pull_request,
847 870 ChangesetComment.pull_request_version == None)\
848 871 .order_by(ChangesetComment.comment_id.asc())
849 872
850 873 # TODO: johbo: Find out why this breaks if it is done in a bulk
851 874 # operation.
852 875 for comment in comments:
853 876 comment.pull_request_version_id = (
854 877 pull_request_version.pull_request_version_id)
855 878 Session().add(comment)
856 879
857 880 def _calculate_commit_id_changes(self, old_ids, new_ids):
858 881 added = [x for x in new_ids if x not in old_ids]
859 882 common = [x for x in new_ids if x in old_ids]
860 883 removed = [x for x in old_ids if x not in new_ids]
861 884 total = new_ids
862 885 return ChangeTuple(added, common, removed, total)
863 886
864 887 def _calculate_file_changes(self, old_diff_data, new_diff_data):
865 888
866 889 old_files = OrderedDict()
867 890 for diff_data in old_diff_data.parsed_diff:
868 891 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
869 892
870 893 added_files = []
871 894 modified_files = []
872 895 removed_files = []
873 896 for diff_data in new_diff_data.parsed_diff:
874 897 new_filename = diff_data['filename']
875 898 new_hash = md5_safe(diff_data['raw_diff'])
876 899
877 900 old_hash = old_files.get(new_filename)
878 901 if not old_hash:
879 902 # file is not present in old diff, means it's added
880 903 added_files.append(new_filename)
881 904 else:
882 905 if new_hash != old_hash:
883 906 modified_files.append(new_filename)
884 907 # now remove a file from old, since we have seen it already
885 908 del old_files[new_filename]
886 909
887 910 # removed files is when there are present in old, but not in NEW,
888 911 # since we remove old files that are present in new diff, left-overs
889 912 # if any should be the removed files
890 913 removed_files.extend(old_files.keys())
891 914
892 915 return FileChangeTuple(added_files, modified_files, removed_files)
893 916
894 917 def _render_update_message(self, changes, file_changes):
895 918 """
896 919 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
897 920 so it's always looking the same disregarding on which default
898 921 renderer system is using.
899 922
900 923 :param changes: changes named tuple
901 924 :param file_changes: file changes named tuple
902 925
903 926 """
904 927 new_status = ChangesetStatus.get_status_lbl(
905 928 ChangesetStatus.STATUS_UNDER_REVIEW)
906 929
907 930 changed_files = (
908 931 file_changes.added + file_changes.modified + file_changes.removed)
909 932
910 933 params = {
911 934 'under_review_label': new_status,
912 935 'added_commits': changes.added,
913 936 'removed_commits': changes.removed,
914 937 'changed_files': changed_files,
915 938 'added_files': file_changes.added,
916 939 'modified_files': file_changes.modified,
917 940 'removed_files': file_changes.removed,
918 941 }
919 942 renderer = RstTemplateRenderer()
920 943 return renderer.render('pull_request_update.mako', **params)
921 944
922 945 def edit(self, pull_request, title, description, user):
923 946 pull_request = self.__get_pull_request(pull_request)
924 947 old_data = pull_request.get_api_data(with_merge_state=False)
925 948 if pull_request.is_closed():
926 949 raise ValueError('This pull request is closed')
927 950 if title:
928 951 pull_request.title = title
929 952 pull_request.description = description
930 953 pull_request.updated_on = datetime.datetime.now()
931 954 Session().add(pull_request)
932 955 self._log_audit_action(
933 956 'repo.pull_request.edit', {'old_data': old_data},
934 957 user, pull_request)
935 958
936 959 def update_reviewers(self, pull_request, reviewer_data, user):
937 960 """
938 961 Update the reviewers in the pull request
939 962
940 963 :param pull_request: the pr to update
941 964 :param reviewer_data: list of tuples
942 965 [(user, ['reason1', 'reason2'], mandatory_flag)]
943 966 """
944 967 pull_request = self.__get_pull_request(pull_request)
945 968 if pull_request.is_closed():
946 969 raise ValueError('This pull request is closed')
947 970
948 971 reviewers = {}
949 972 for user_id, reasons, mandatory in reviewer_data:
950 973 if isinstance(user_id, (int, basestring)):
951 974 user_id = self._get_user(user_id).user_id
952 975 reviewers[user_id] = {
953 976 'reasons': reasons, 'mandatory': mandatory}
954 977
955 978 reviewers_ids = set(reviewers.keys())
956 979 current_reviewers = PullRequestReviewers.query()\
957 980 .filter(PullRequestReviewers.pull_request ==
958 981 pull_request).all()
959 982 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
960 983
961 984 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
962 985 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
963 986
964 987 log.debug("Adding %s reviewers", ids_to_add)
965 988 log.debug("Removing %s reviewers", ids_to_remove)
966 989 changed = False
967 990 for uid in ids_to_add:
968 991 changed = True
969 992 _usr = self._get_user(uid)
970 993 reviewer = PullRequestReviewers()
971 994 reviewer.user = _usr
972 995 reviewer.pull_request = pull_request
973 996 reviewer.reasons = reviewers[uid]['reasons']
974 997 # NOTE(marcink): mandatory shouldn't be changed now
975 998 # reviewer.mandatory = reviewers[uid]['reasons']
976 999 Session().add(reviewer)
977 1000 self._log_audit_action(
978 1001 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
979 1002 user, pull_request)
980 1003
981 1004 for uid in ids_to_remove:
982 1005 changed = True
983 1006 reviewers = PullRequestReviewers.query()\
984 1007 .filter(PullRequestReviewers.user_id == uid,
985 1008 PullRequestReviewers.pull_request == pull_request)\
986 1009 .all()
987 1010 # use .all() in case we accidentally added the same person twice
988 1011 # this CAN happen due to the lack of DB checks
989 1012 for obj in reviewers:
990 1013 old_data = obj.get_dict()
991 1014 Session().delete(obj)
992 1015 self._log_audit_action(
993 1016 'repo.pull_request.reviewer.delete',
994 1017 {'old_data': old_data}, user, pull_request)
995 1018
996 1019 if changed:
997 1020 pull_request.updated_on = datetime.datetime.now()
998 1021 Session().add(pull_request)
999 1022
1000 1023 self.notify_reviewers(pull_request, ids_to_add)
1001 1024 return ids_to_add, ids_to_remove
1002 1025
1003 1026 def get_url(self, pull_request, request=None, permalink=False):
1004 1027 if not request:
1005 1028 request = get_current_request()
1006 1029
1007 1030 if permalink:
1008 1031 return request.route_url(
1009 1032 'pull_requests_global',
1010 1033 pull_request_id=pull_request.pull_request_id,)
1011 1034 else:
1012 1035 return request.route_url('pullrequest_show',
1013 1036 repo_name=safe_str(pull_request.target_repo.repo_name),
1014 1037 pull_request_id=pull_request.pull_request_id,)
1015 1038
1016 1039 def get_shadow_clone_url(self, pull_request):
1017 1040 """
1018 1041 Returns qualified url pointing to the shadow repository. If this pull
1019 1042 request is closed there is no shadow repository and ``None`` will be
1020 1043 returned.
1021 1044 """
1022 1045 if pull_request.is_closed():
1023 1046 return None
1024 1047 else:
1025 1048 pr_url = urllib.unquote(self.get_url(pull_request))
1026 1049 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1027 1050
1028 1051 def notify_reviewers(self, pull_request, reviewers_ids):
1029 1052 # notification to reviewers
1030 1053 if not reviewers_ids:
1031 1054 return
1032 1055
1033 1056 pull_request_obj = pull_request
1034 1057 # get the current participants of this pull request
1035 1058 recipients = reviewers_ids
1036 1059 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1037 1060
1038 1061 pr_source_repo = pull_request_obj.source_repo
1039 1062 pr_target_repo = pull_request_obj.target_repo
1040 1063
1041 1064 pr_url = h.route_url('pullrequest_show',
1042 1065 repo_name=pr_target_repo.repo_name,
1043 1066 pull_request_id=pull_request_obj.pull_request_id,)
1044 1067
1045 1068 # set some variables for email notification
1046 1069 pr_target_repo_url = h.route_url(
1047 1070 'repo_summary', repo_name=pr_target_repo.repo_name)
1048 1071
1049 1072 pr_source_repo_url = h.route_url(
1050 1073 'repo_summary', repo_name=pr_source_repo.repo_name)
1051 1074
1052 1075 # pull request specifics
1053 1076 pull_request_commits = [
1054 1077 (x.raw_id, x.message)
1055 1078 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1056 1079
1057 1080 kwargs = {
1058 1081 'user': pull_request.author,
1059 1082 'pull_request': pull_request_obj,
1060 1083 'pull_request_commits': pull_request_commits,
1061 1084
1062 1085 'pull_request_target_repo': pr_target_repo,
1063 1086 'pull_request_target_repo_url': pr_target_repo_url,
1064 1087
1065 1088 'pull_request_source_repo': pr_source_repo,
1066 1089 'pull_request_source_repo_url': pr_source_repo_url,
1067 1090
1068 1091 'pull_request_url': pr_url,
1069 1092 }
1070 1093
1071 1094 # pre-generate the subject for notification itself
1072 1095 (subject,
1073 1096 _h, _e, # we don't care about those
1074 1097 body_plaintext) = EmailNotificationModel().render_email(
1075 1098 notification_type, **kwargs)
1076 1099
1077 1100 # create notification objects, and emails
1078 1101 NotificationModel().create(
1079 1102 created_by=pull_request.author,
1080 1103 notification_subject=subject,
1081 1104 notification_body=body_plaintext,
1082 1105 notification_type=notification_type,
1083 1106 recipients=recipients,
1084 1107 email_kwargs=kwargs,
1085 1108 )
1086 1109
1087 1110 def delete(self, pull_request, user):
1088 1111 pull_request = self.__get_pull_request(pull_request)
1089 1112 old_data = pull_request.get_api_data(with_merge_state=False)
1090 1113 self._cleanup_merge_workspace(pull_request)
1091 1114 self._log_audit_action(
1092 1115 'repo.pull_request.delete', {'old_data': old_data},
1093 1116 user, pull_request)
1094 1117 Session().delete(pull_request)
1095 1118
1096 1119 def close_pull_request(self, pull_request, user):
1097 1120 pull_request = self.__get_pull_request(pull_request)
1098 1121 self._cleanup_merge_workspace(pull_request)
1099 1122 pull_request.status = PullRequest.STATUS_CLOSED
1100 1123 pull_request.updated_on = datetime.datetime.now()
1101 1124 Session().add(pull_request)
1102 1125 self._trigger_pull_request_hook(
1103 1126 pull_request, pull_request.author, 'close')
1104 1127
1105 1128 pr_data = pull_request.get_api_data(with_merge_state=False)
1106 1129 self._log_audit_action(
1107 1130 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1108 1131
1109 1132 def close_pull_request_with_comment(
1110 1133 self, pull_request, user, repo, message=None):
1111 1134
1112 1135 pull_request_review_status = pull_request.calculated_review_status()
1113 1136
1114 1137 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1115 1138 # approved only if we have voting consent
1116 1139 status = ChangesetStatus.STATUS_APPROVED
1117 1140 else:
1118 1141 status = ChangesetStatus.STATUS_REJECTED
1119 1142 status_lbl = ChangesetStatus.get_status_lbl(status)
1120 1143
1121 1144 default_message = (
1122 1145 'Closing with status change {transition_icon} {status}.'
1123 1146 ).format(transition_icon='>', status=status_lbl)
1124 1147 text = message or default_message
1125 1148
1126 1149 # create a comment, and link it to new status
1127 1150 comment = CommentsModel().create(
1128 1151 text=text,
1129 1152 repo=repo.repo_id,
1130 1153 user=user.user_id,
1131 1154 pull_request=pull_request.pull_request_id,
1132 1155 status_change=status_lbl,
1133 1156 status_change_type=status,
1134 1157 closing_pr=True
1135 1158 )
1136 1159
1137 1160 # calculate old status before we change it
1138 1161 old_calculated_status = pull_request.calculated_review_status()
1139 1162 ChangesetStatusModel().set_status(
1140 1163 repo.repo_id,
1141 1164 status,
1142 1165 user.user_id,
1143 1166 comment=comment,
1144 1167 pull_request=pull_request.pull_request_id
1145 1168 )
1146 1169
1147 1170 Session().flush()
1148 1171 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1149 1172 # we now calculate the status of pull request again, and based on that
1150 1173 # calculation trigger status change. This might happen in cases
1151 1174 # that non-reviewer admin closes a pr, which means his vote doesn't
1152 1175 # change the status, while if he's a reviewer this might change it.
1153 1176 calculated_status = pull_request.calculated_review_status()
1154 1177 if old_calculated_status != calculated_status:
1155 1178 self._trigger_pull_request_hook(
1156 1179 pull_request, user, 'review_status_change')
1157 1180
1158 1181 # finally close the PR
1159 1182 PullRequestModel().close_pull_request(
1160 1183 pull_request.pull_request_id, user)
1161 1184
1162 1185 return comment, status
1163 1186
1164 1187 def merge_status(self, pull_request, translator=None):
1165 1188 _ = translator or get_current_request().translate
1166 1189
1167 1190 if not self._is_merge_enabled(pull_request):
1168 1191 return False, _('Server-side pull request merging is disabled.')
1169 1192 if pull_request.is_closed():
1170 1193 return False, _('This pull request is closed.')
1171 1194 merge_possible, msg = self._check_repo_requirements(
1172 1195 target=pull_request.target_repo, source=pull_request.source_repo,
1173 1196 translator=_)
1174 1197 if not merge_possible:
1175 1198 return merge_possible, msg
1176 1199
1177 1200 try:
1178 1201 resp = self._try_merge(pull_request)
1179 1202 log.debug("Merge response: %s", resp)
1180 1203 status = resp.possible, self.merge_status_message(
1181 1204 resp.failure_reason)
1182 1205 except NotImplementedError:
1183 1206 status = False, _('Pull request merging is not supported.')
1184 1207
1185 1208 return status
1186 1209
1187 1210 def _check_repo_requirements(self, target, source, translator):
1188 1211 """
1189 1212 Check if `target` and `source` have compatible requirements.
1190 1213
1191 1214 Currently this is just checking for largefiles.
1192 1215 """
1193 1216 _ = translator
1194 1217 target_has_largefiles = self._has_largefiles(target)
1195 1218 source_has_largefiles = self._has_largefiles(source)
1196 1219 merge_possible = True
1197 1220 message = u''
1198 1221
1199 1222 if target_has_largefiles != source_has_largefiles:
1200 1223 merge_possible = False
1201 1224 if source_has_largefiles:
1202 1225 message = _(
1203 1226 'Target repository large files support is disabled.')
1204 1227 else:
1205 1228 message = _(
1206 1229 'Source repository large files support is disabled.')
1207 1230
1208 1231 return merge_possible, message
1209 1232
1210 1233 def _has_largefiles(self, repo):
1211 1234 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1212 1235 'extensions', 'largefiles')
1213 1236 return largefiles_ui and largefiles_ui[0].active
1214 1237
1215 1238 def _try_merge(self, pull_request):
1216 1239 """
1217 1240 Try to merge the pull request and return the merge status.
1218 1241 """
1219 1242 log.debug(
1220 1243 "Trying out if the pull request %s can be merged.",
1221 1244 pull_request.pull_request_id)
1222 1245 target_vcs = pull_request.target_repo.scm_instance()
1223 1246
1224 1247 # Refresh the target reference.
1225 1248 try:
1226 1249 target_ref = self._refresh_reference(
1227 1250 pull_request.target_ref_parts, target_vcs)
1228 1251 except CommitDoesNotExistError:
1229 1252 merge_state = MergeResponse(
1230 1253 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1231 1254 return merge_state
1232 1255
1233 1256 target_locked = pull_request.target_repo.locked
1234 1257 if target_locked and target_locked[0]:
1235 1258 log.debug("The target repository is locked.")
1236 1259 merge_state = MergeResponse(
1237 1260 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1238 1261 elif self._needs_merge_state_refresh(pull_request, target_ref):
1239 1262 log.debug("Refreshing the merge status of the repository.")
1240 1263 merge_state = self._refresh_merge_state(
1241 1264 pull_request, target_vcs, target_ref)
1242 1265 else:
1243 1266 possible = pull_request.\
1244 1267 last_merge_status == MergeFailureReason.NONE
1245 1268 merge_state = MergeResponse(
1246 1269 possible, False, None, pull_request.last_merge_status)
1247 1270
1248 1271 return merge_state
1249 1272
1250 1273 def _refresh_reference(self, reference, vcs_repository):
1251 1274 if reference.type in ('branch', 'book'):
1252 1275 name_or_id = reference.name
1253 1276 else:
1254 1277 name_or_id = reference.commit_id
1255 1278 refreshed_commit = vcs_repository.get_commit(name_or_id)
1256 1279 refreshed_reference = Reference(
1257 1280 reference.type, reference.name, refreshed_commit.raw_id)
1258 1281 return refreshed_reference
1259 1282
1260 1283 def _needs_merge_state_refresh(self, pull_request, target_reference):
1261 1284 return not(
1262 1285 pull_request.revisions and
1263 1286 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1264 1287 target_reference.commit_id == pull_request._last_merge_target_rev)
1265 1288
1266 1289 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1267 1290 workspace_id = self._workspace_id(pull_request)
1268 1291 source_vcs = pull_request.source_repo.scm_instance()
1269 1292 use_rebase = self._use_rebase_for_merging(pull_request)
1270 1293 close_branch = self._close_branch_before_merging(pull_request)
1271 1294 merge_state = target_vcs.merge(
1272 1295 target_reference, source_vcs, pull_request.source_ref_parts,
1273 1296 workspace_id, dry_run=True, use_rebase=use_rebase,
1274 1297 close_branch=close_branch)
1275 1298
1276 1299 # Do not store the response if there was an unknown error.
1277 1300 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1278 1301 pull_request._last_merge_source_rev = \
1279 1302 pull_request.source_ref_parts.commit_id
1280 1303 pull_request._last_merge_target_rev = target_reference.commit_id
1281 1304 pull_request.last_merge_status = merge_state.failure_reason
1282 1305 pull_request.shadow_merge_ref = merge_state.merge_ref
1283 1306 Session().add(pull_request)
1284 1307 Session().commit()
1285 1308
1286 1309 return merge_state
1287 1310
1288 1311 def _workspace_id(self, pull_request):
1289 1312 workspace_id = 'pr-%s' % pull_request.pull_request_id
1290 1313 return workspace_id
1291 1314
1292 1315 def merge_status_message(self, status_code):
1293 1316 """
1294 1317 Return a human friendly error message for the given merge status code.
1295 1318 """
1296 1319 return self.MERGE_STATUS_MESSAGES[status_code]
1297 1320
1298 1321 def generate_repo_data(self, repo, commit_id=None, branch=None,
1299 1322 bookmark=None, translator=None):
1300 1323
1301 1324 all_refs, selected_ref = \
1302 1325 self._get_repo_pullrequest_sources(
1303 1326 repo.scm_instance(), commit_id=commit_id,
1304 1327 branch=branch, bookmark=bookmark, translator=translator)
1305 1328
1306 1329 refs_select2 = []
1307 1330 for element in all_refs:
1308 1331 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1309 1332 refs_select2.append({'text': element[1], 'children': children})
1310 1333
1311 1334 return {
1312 1335 'user': {
1313 1336 'user_id': repo.user.user_id,
1314 1337 'username': repo.user.username,
1315 1338 'firstname': repo.user.first_name,
1316 1339 'lastname': repo.user.last_name,
1317 1340 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1318 1341 },
1319 1342 'description': h.chop_at_smart(repo.description_safe, '\n'),
1320 1343 'refs': {
1321 1344 'all_refs': all_refs,
1322 1345 'selected_ref': selected_ref,
1323 1346 'select2_refs': refs_select2
1324 1347 }
1325 1348 }
1326 1349
1327 1350 def generate_pullrequest_title(self, source, source_ref, target):
1328 1351 return u'{source}#{at_ref} to {target}'.format(
1329 1352 source=source,
1330 1353 at_ref=source_ref,
1331 1354 target=target,
1332 1355 )
1333 1356
1334 1357 def _cleanup_merge_workspace(self, pull_request):
1335 1358 # Merging related cleanup
1336 1359 target_scm = pull_request.target_repo.scm_instance()
1337 1360 workspace_id = 'pr-%s' % pull_request.pull_request_id
1338 1361
1339 1362 try:
1340 1363 target_scm.cleanup_merge_workspace(workspace_id)
1341 1364 except NotImplementedError:
1342 1365 pass
1343 1366
1344 1367 def _get_repo_pullrequest_sources(
1345 1368 self, repo, commit_id=None, branch=None, bookmark=None,
1346 1369 translator=None):
1347 1370 """
1348 1371 Return a structure with repo's interesting commits, suitable for
1349 1372 the selectors in pullrequest controller
1350 1373
1351 1374 :param commit_id: a commit that must be in the list somehow
1352 1375 and selected by default
1353 1376 :param branch: a branch that must be in the list and selected
1354 1377 by default - even if closed
1355 1378 :param bookmark: a bookmark that must be in the list and selected
1356 1379 """
1357 1380 _ = translator or get_current_request().translate
1358 1381
1359 1382 commit_id = safe_str(commit_id) if commit_id else None
1360 1383 branch = safe_str(branch) if branch else None
1361 1384 bookmark = safe_str(bookmark) if bookmark else None
1362 1385
1363 1386 selected = None
1364 1387
1365 1388 # order matters: first source that has commit_id in it will be selected
1366 1389 sources = []
1367 1390 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1368 1391 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1369 1392
1370 1393 if commit_id:
1371 1394 ref_commit = (h.short_id(commit_id), commit_id)
1372 1395 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1373 1396
1374 1397 sources.append(
1375 1398 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1376 1399 )
1377 1400
1378 1401 groups = []
1379 1402 for group_key, ref_list, group_name, match in sources:
1380 1403 group_refs = []
1381 1404 for ref_name, ref_id in ref_list:
1382 1405 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1383 1406 group_refs.append((ref_key, ref_name))
1384 1407
1385 1408 if not selected:
1386 1409 if set([commit_id, match]) & set([ref_id, ref_name]):
1387 1410 selected = ref_key
1388 1411
1389 1412 if group_refs:
1390 1413 groups.append((group_refs, group_name))
1391 1414
1392 1415 if not selected:
1393 1416 ref = commit_id or branch or bookmark
1394 1417 if ref:
1395 1418 raise CommitDoesNotExistError(
1396 1419 'No commit refs could be found matching: %s' % ref)
1397 1420 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1398 1421 selected = 'branch:%s:%s' % (
1399 1422 repo.DEFAULT_BRANCH_NAME,
1400 1423 repo.branches[repo.DEFAULT_BRANCH_NAME]
1401 1424 )
1402 1425 elif repo.commit_ids:
1403 1426 rev = repo.commit_ids[0]
1404 1427 selected = 'rev:%s:%s' % (rev, rev)
1405 1428 else:
1406 1429 raise EmptyRepositoryError()
1407 1430 return groups, selected
1408 1431
1409 1432 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1410 1433 return self._get_diff_from_pr_or_version(
1411 1434 source_repo, source_ref_id, target_ref_id, context=context)
1412 1435
1413 1436 def _get_diff_from_pr_or_version(
1414 1437 self, source_repo, source_ref_id, target_ref_id, context):
1415 1438 target_commit = source_repo.get_commit(
1416 1439 commit_id=safe_str(target_ref_id))
1417 1440 source_commit = source_repo.get_commit(
1418 1441 commit_id=safe_str(source_ref_id))
1419 1442 if isinstance(source_repo, Repository):
1420 1443 vcs_repo = source_repo.scm_instance()
1421 1444 else:
1422 1445 vcs_repo = source_repo
1423 1446
1424 1447 # TODO: johbo: In the context of an update, we cannot reach
1425 1448 # the old commit anymore with our normal mechanisms. It needs
1426 1449 # some sort of special support in the vcs layer to avoid this
1427 1450 # workaround.
1428 1451 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1429 1452 vcs_repo.alias == 'git'):
1430 1453 source_commit.raw_id = safe_str(source_ref_id)
1431 1454
1432 1455 log.debug('calculating diff between '
1433 1456 'source_ref:%s and target_ref:%s for repo `%s`',
1434 1457 target_ref_id, source_ref_id,
1435 1458 safe_unicode(vcs_repo.path))
1436 1459
1437 1460 vcs_diff = vcs_repo.get_diff(
1438 1461 commit1=target_commit, commit2=source_commit, context=context)
1439 1462 return vcs_diff
1440 1463
1441 1464 def _is_merge_enabled(self, pull_request):
1442 1465 return self._get_general_setting(
1443 1466 pull_request, 'rhodecode_pr_merge_enabled')
1444 1467
1445 1468 def _use_rebase_for_merging(self, pull_request):
1446 1469 repo_type = pull_request.target_repo.repo_type
1447 1470 if repo_type == 'hg':
1448 1471 return self._get_general_setting(
1449 1472 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1450 1473 elif repo_type == 'git':
1451 1474 return self._get_general_setting(
1452 1475 pull_request, 'rhodecode_git_use_rebase_for_merging')
1453 1476
1454 1477 return False
1455 1478
1456 1479 def _close_branch_before_merging(self, pull_request):
1457 1480 repo_type = pull_request.target_repo.repo_type
1458 1481 if repo_type == 'hg':
1459 1482 return self._get_general_setting(
1460 1483 pull_request, 'rhodecode_hg_close_branch_before_merging')
1461 1484 elif repo_type == 'git':
1462 1485 return self._get_general_setting(
1463 1486 pull_request, 'rhodecode_git_close_branch_before_merging')
1464 1487
1465 1488 return False
1466 1489
1467 1490 def _get_general_setting(self, pull_request, settings_key, default=False):
1468 1491 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1469 1492 settings = settings_model.get_general_settings()
1470 1493 return settings.get(settings_key, default)
1471 1494
1472 1495 def _log_audit_action(self, action, action_data, user, pull_request):
1473 1496 audit_logger.store(
1474 1497 action=action,
1475 1498 action_data=action_data,
1476 1499 user=user,
1477 1500 repo=pull_request.target_repo)
1478 1501
1479 1502 def get_reviewer_functions(self):
1480 1503 """
1481 1504 Fetches functions for validation and fetching default reviewers.
1482 1505 If available we use the EE package, else we fallback to CE
1483 1506 package functions
1484 1507 """
1485 1508 try:
1486 1509 from rc_reviewers.utils import get_default_reviewers_data
1487 1510 from rc_reviewers.utils import validate_default_reviewers
1488 1511 except ImportError:
1489 1512 from rhodecode.apps.repository.utils import \
1490 1513 get_default_reviewers_data
1491 1514 from rhodecode.apps.repository.utils import \
1492 1515 validate_default_reviewers
1493 1516
1494 1517 return get_default_reviewers_data, validate_default_reviewers
1495 1518
1496 1519
1497 1520 class MergeCheck(object):
1498 1521 """
1499 1522 Perform Merge Checks and returns a check object which stores information
1500 1523 about merge errors, and merge conditions
1501 1524 """
1502 1525 TODO_CHECK = 'todo'
1503 1526 PERM_CHECK = 'perm'
1504 1527 REVIEW_CHECK = 'review'
1505 1528 MERGE_CHECK = 'merge'
1506 1529
1507 1530 def __init__(self):
1508 1531 self.review_status = None
1509 1532 self.merge_possible = None
1510 1533 self.merge_msg = ''
1511 1534 self.failed = None
1512 1535 self.errors = []
1513 1536 self.error_details = OrderedDict()
1514 1537
1515 1538 def push_error(self, error_type, message, error_key, details):
1516 1539 self.failed = True
1517 1540 self.errors.append([error_type, message])
1518 1541 self.error_details[error_key] = dict(
1519 1542 details=details,
1520 1543 error_type=error_type,
1521 1544 message=message
1522 1545 )
1523 1546
1524 1547 @classmethod
1525 1548 def validate(cls, pull_request, user, translator, fail_early=False):
1526 1549 _ = translator
1527 1550 merge_check = cls()
1528 1551
1529 1552 # permissions to merge
1530 1553 user_allowed_to_merge = PullRequestModel().check_user_merge(
1531 1554 pull_request, user)
1532 1555 if not user_allowed_to_merge:
1533 1556 log.debug("MergeCheck: cannot merge, approval is pending.")
1534 1557
1535 1558 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1536 1559 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1537 1560 if fail_early:
1538 1561 return merge_check
1539 1562
1540 1563 # review status, must be always present
1541 1564 review_status = pull_request.calculated_review_status()
1542 1565 merge_check.review_status = review_status
1543 1566
1544 1567 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1545 1568 if not status_approved:
1546 1569 log.debug("MergeCheck: cannot merge, approval is pending.")
1547 1570
1548 1571 msg = _('Pull request reviewer approval is pending.')
1549 1572
1550 1573 merge_check.push_error(
1551 1574 'warning', msg, cls.REVIEW_CHECK, review_status)
1552 1575
1553 1576 if fail_early:
1554 1577 return merge_check
1555 1578
1556 1579 # left over TODOs
1557 1580 todos = CommentsModel().get_unresolved_todos(pull_request)
1558 1581 if todos:
1559 1582 log.debug("MergeCheck: cannot merge, {} "
1560 1583 "unresolved todos left.".format(len(todos)))
1561 1584
1562 1585 if len(todos) == 1:
1563 1586 msg = _('Cannot merge, {} TODO still not resolved.').format(
1564 1587 len(todos))
1565 1588 else:
1566 1589 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1567 1590 len(todos))
1568 1591
1569 1592 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1570 1593
1571 1594 if fail_early:
1572 1595 return merge_check
1573 1596
1574 1597 # merge possible
1575 1598 merge_status, msg = PullRequestModel().merge_status(
1576 1599 pull_request, translator=translator)
1577 1600 merge_check.merge_possible = merge_status
1578 1601 merge_check.merge_msg = msg
1579 1602 if not merge_status:
1580 1603 log.debug(
1581 1604 "MergeCheck: cannot merge, pull request merge not possible.")
1582 1605 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1583 1606
1584 1607 if fail_early:
1585 1608 return merge_check
1586 1609
1587 1610 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1588 1611 return merge_check
1589 1612
1590 1613 @classmethod
1591 1614 def get_merge_conditions(cls, pull_request, translator):
1592 1615 _ = translator
1593 1616 merge_details = {}
1594 1617
1595 1618 model = PullRequestModel()
1596 1619 use_rebase = model._use_rebase_for_merging(pull_request)
1597 1620
1598 1621 if use_rebase:
1599 1622 merge_details['merge_strategy'] = dict(
1600 1623 details={},
1601 1624 message=_('Merge strategy: rebase')
1602 1625 )
1603 1626 else:
1604 1627 merge_details['merge_strategy'] = dict(
1605 1628 details={},
1606 1629 message=_('Merge strategy: explicit merge commit')
1607 1630 )
1608 1631
1609 1632 close_branch = model._close_branch_before_merging(pull_request)
1610 1633 if close_branch:
1611 1634 repo_type = pull_request.target_repo.repo_type
1612 1635 if repo_type == 'hg':
1613 1636 close_msg = _('Source branch will be closed after merge.')
1614 1637 elif repo_type == 'git':
1615 1638 close_msg = _('Source branch will be deleted after merge.')
1616 1639
1617 1640 merge_details['close_branch'] = dict(
1618 1641 details={},
1619 1642 message=close_msg
1620 1643 )
1621 1644
1622 1645 return merge_details
1623 1646
1624 1647 ChangeTuple = collections.namedtuple(
1625 1648 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1626 1649
1627 1650 FileChangeTuple = collections.namedtuple(
1628 1651 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now