##// END OF EJS Templates
pull-requests: expose commit versions in the pull-request commit list. Fixes #5642
milka -
r4615:ca0827b2 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1854 +1,1857 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 110 self.db_repo.repo_id, pull_request=pr,
111 111 include_drafts=False, count_only=True)
112 112
113 113 data.append({
114 114 'name': _render('pullrequest_name',
115 115 pr.pull_request_id, pr.pull_request_state,
116 116 pr.work_in_progress, pr.target_repo.repo_name,
117 117 short=True),
118 118 'name_raw': pr.pull_request_id,
119 119 'status': _render('pullrequest_status',
120 120 pr.calculated_review_status()),
121 121 'title': _render('pullrequest_title', pr.title, pr.description),
122 122 'description': h.escape(pr.description),
123 123 'updated_on': _render('pullrequest_updated_on',
124 124 h.datetime_to_time(pr.updated_on),
125 125 pr.versions_count),
126 126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 127 'created_on': _render('pullrequest_updated_on',
128 128 h.datetime_to_time(pr.created_on)),
129 129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 130 'state': pr.pull_request_state,
131 131 'author': _render('pullrequest_author',
132 132 pr.author.full_contact, ),
133 133 'author_raw': pr.author.full_name,
134 134 'comments': _render('pullrequest_comments', comments_count),
135 135 'comments_raw': comments_count,
136 136 'closed': pr.is_closed(),
137 137 })
138 138
139 139 data = ({
140 140 'draw': draw,
141 141 'data': data,
142 142 'recordsTotal': pull_requests_total_count,
143 143 'recordsFiltered': pull_requests_total_count,
144 144 })
145 145 return data
146 146
147 147 @LoginRequired()
148 148 @HasRepoPermissionAnyDecorator(
149 149 'repository.read', 'repository.write', 'repository.admin')
150 150 def pull_request_list(self):
151 151 c = self.load_default_context()
152 152
153 153 req_get = self.request.GET
154 154 c.source = str2bool(req_get.get('source'))
155 155 c.closed = str2bool(req_get.get('closed'))
156 156 c.my = str2bool(req_get.get('my'))
157 157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159 159
160 160 c.active = 'open'
161 161 if c.my:
162 162 c.active = 'my'
163 163 if c.closed:
164 164 c.active = 'closed'
165 165 if c.awaiting_review and not c.source:
166 166 c.active = 'awaiting'
167 167 if c.source and not c.awaiting_review:
168 168 c.active = 'source'
169 169 if c.awaiting_my_review:
170 170 c.active = 'awaiting_my'
171 171
172 172 return self._get_template_context(c)
173 173
174 174 @LoginRequired()
175 175 @HasRepoPermissionAnyDecorator(
176 176 'repository.read', 'repository.write', 'repository.admin')
177 177 def pull_request_list_data(self):
178 178 self.load_default_context()
179 179
180 180 # additional filters
181 181 req_get = self.request.GET
182 182 source = str2bool(req_get.get('source'))
183 183 closed = str2bool(req_get.get('closed'))
184 184 my = str2bool(req_get.get('my'))
185 185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 187
188 188 filter_type = 'awaiting_review' if awaiting_review \
189 189 else 'awaiting_my_review' if awaiting_my_review \
190 190 else None
191 191
192 192 opened_by = None
193 193 if my:
194 194 opened_by = [self._rhodecode_user.user_id]
195 195
196 196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 197 if closed:
198 198 statuses = [PullRequest.STATUS_CLOSED]
199 199
200 200 data = self._get_pull_requests_list(
201 201 repo_name=self.db_repo_name, source=source,
202 202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 203
204 204 return data
205 205
206 206 def _is_diff_cache_enabled(self, target_repo):
207 207 caching_enabled = self._get_general_setting(
208 208 target_repo, 'rhodecode_diff_cache')
209 209 log.debug('Diff caching enabled: %s', caching_enabled)
210 210 return caching_enabled
211 211
212 212 def _get_diffset(self, source_repo_name, source_repo,
213 213 ancestor_commit,
214 214 source_ref_id, target_ref_id,
215 215 target_commit, source_commit, diff_limit, file_limit,
216 216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 217
218 218 target_commit_final = target_commit
219 219 source_commit_final = source_commit
220 220
221 221 if use_ancestor:
222 222 # we might want to not use it for versions
223 223 target_ref_id = ancestor_commit.raw_id
224 224 target_commit_final = ancestor_commit
225 225
226 226 vcs_diff = PullRequestModel().get_diff(
227 227 source_repo, source_ref_id, target_ref_id,
228 228 hide_whitespace_changes, diff_context)
229 229
230 230 diff_processor = diffs.DiffProcessor(
231 231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 232 file_limit=file_limit, show_full_diff=fulldiff)
233 233
234 234 _parsed = diff_processor.prepare()
235 235
236 236 diffset = codeblocks.DiffSet(
237 237 repo_name=self.db_repo_name,
238 238 source_repo_name=source_repo_name,
239 239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 241 )
242 242 diffset = self.path_filter.render_patchset_filtered(
243 243 diffset, _parsed, target_ref_id, source_ref_id)
244 244
245 245 return diffset
246 246
247 247 def _get_range_diffset(self, source_scm, source_repo,
248 248 commit1, commit2, diff_limit, file_limit,
249 249 fulldiff, hide_whitespace_changes, diff_context):
250 250 vcs_diff = source_scm.get_diff(
251 251 commit1, commit2,
252 252 ignore_whitespace=hide_whitespace_changes,
253 253 context=diff_context)
254 254
255 255 diff_processor = diffs.DiffProcessor(
256 256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 257 file_limit=file_limit, show_full_diff=fulldiff)
258 258
259 259 _parsed = diff_processor.prepare()
260 260
261 261 diffset = codeblocks.DiffSet(
262 262 repo_name=source_repo.repo_name,
263 263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265 265
266 266 diffset = self.path_filter.render_patchset_filtered(
267 267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268 268
269 269 return diffset
270 270
271 271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 272 comments_model = CommentsModel()
273 273
274 274 # GENERAL COMMENTS with versions #
275 275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 276 q = q.order_by(ChangesetComment.comment_id.asc())
277 277 if not include_drafts:
278 278 q = q.filter(ChangesetComment.draft == false())
279 279 general_comments = q
280 280
281 281 # pick comments we want to render at current version
282 282 c.comment_versions = comments_model.aggregate_comments(
283 283 general_comments, versions, c.at_version_num)
284 284
285 285 # INLINE COMMENTS with versions #
286 286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 287 q = q.order_by(ChangesetComment.comment_id.asc())
288 288 if not include_drafts:
289 289 q = q.filter(ChangesetComment.draft == false())
290 290 inline_comments = q
291 291
292 292 c.inline_versions = comments_model.aggregate_comments(
293 293 inline_comments, versions, c.at_version_num, inline=True)
294 294
295 295 # Comments inline+general
296 296 if c.at_version:
297 297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 298 c.comments = c.comment_versions[c.at_version_num]['display']
299 299 else:
300 300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 301 c.comments = c.comment_versions[c.at_version_num]['until']
302 302
303 303 return general_comments, inline_comments
304 304
305 305 @LoginRequired()
306 306 @HasRepoPermissionAnyDecorator(
307 307 'repository.read', 'repository.write', 'repository.admin')
308 308 def pull_request_show(self):
309 309 _ = self.request.translate
310 310 c = self.load_default_context()
311 311
312 312 pull_request = PullRequest.get_or_404(
313 313 self.request.matchdict['pull_request_id'])
314 314 pull_request_id = pull_request.pull_request_id
315 315
316 316 c.state_progressing = pull_request.is_state_changing()
317 317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318 318
319 319 _new_state = {
320 320 'created': PullRequest.STATE_CREATED,
321 321 }.get(self.request.GET.get('force_state'))
322 322
323 323 if c.is_super_admin and _new_state:
324 324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 325 h.flash(
326 326 _('Pull Request state was force changed to `{}`').format(_new_state),
327 327 category='success')
328 328 Session().commit()
329 329
330 330 raise HTTPFound(h.route_path(
331 331 'pullrequest_show', repo_name=self.db_repo_name,
332 332 pull_request_id=pull_request_id))
333 333
334 334 version = self.request.GET.get('version')
335 335 from_version = self.request.GET.get('from_version') or version
336 336 merge_checks = self.request.GET.get('merge_checks')
337 337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340 340
341 341 # fetch global flags of ignore ws or context lines
342 342 diff_context = diffs.get_diff_context(self.request)
343 343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344 344
345 345 (pull_request_latest,
346 346 pull_request_at_ver,
347 347 pull_request_display_obj,
348 348 at_version) = PullRequestModel().get_pr_version(
349 349 pull_request_id, version=version)
350 350
351 351 pr_closed = pull_request_latest.is_closed()
352 352
353 353 if pr_closed and (version or from_version):
354 354 # not allow to browse versions for closed PR
355 355 raise HTTPFound(h.route_path(
356 356 'pullrequest_show', repo_name=self.db_repo_name,
357 357 pull_request_id=pull_request_id))
358 358
359 359 versions = pull_request_display_obj.versions()
360
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362
360 363 # used to store per-commit range diffs
361 364 c.changes = collections.OrderedDict()
362 365
363 366 c.at_version = at_version
364 367 c.at_version_num = (at_version
365 368 if at_version and at_version != PullRequest.LATEST_VER
366 369 else None)
367 370
368 371 c.at_version_index = ChangesetComment.get_index_from_version(
369 372 c.at_version_num, versions)
370 373
371 374 (prev_pull_request_latest,
372 375 prev_pull_request_at_ver,
373 376 prev_pull_request_display_obj,
374 377 prev_at_version) = PullRequestModel().get_pr_version(
375 378 pull_request_id, version=from_version)
376 379
377 380 c.from_version = prev_at_version
378 381 c.from_version_num = (prev_at_version
379 382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
380 383 else None)
381 384 c.from_version_index = ChangesetComment.get_index_from_version(
382 385 c.from_version_num, versions)
383 386
384 387 # define if we're in COMPARE mode or VIEW at version mode
385 388 compare = at_version != prev_at_version
386 389
387 390 # pull_requests repo_name we opened it against
388 391 # ie. target_repo must match
389 392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
390 393 log.warning('Mismatch between the current repo: %s, and target %s',
391 394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
392 395 raise HTTPNotFound()
393 396
394 397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
395 398
396 399 c.pull_request = pull_request_display_obj
397 400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
398 401 c.pull_request_latest = pull_request_latest
399 402
400 403 # inject latest version
401 404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
402 405 c.versions = versions + [latest_ver]
403 406
404 407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
405 408 c.allowed_to_change_status = False
406 409 c.allowed_to_update = False
407 410 c.allowed_to_merge = False
408 411 c.allowed_to_delete = False
409 412 c.allowed_to_comment = False
410 413 c.allowed_to_close = False
411 414 else:
412 415 can_change_status = PullRequestModel().check_user_change_status(
413 416 pull_request_at_ver, self._rhodecode_user)
414 417 c.allowed_to_change_status = can_change_status and not pr_closed
415 418
416 419 c.allowed_to_update = PullRequestModel().check_user_update(
417 420 pull_request_latest, self._rhodecode_user) and not pr_closed
418 421 c.allowed_to_merge = PullRequestModel().check_user_merge(
419 422 pull_request_latest, self._rhodecode_user) and not pr_closed
420 423 c.allowed_to_delete = PullRequestModel().check_user_delete(
421 424 pull_request_latest, self._rhodecode_user) and not pr_closed
422 425 c.allowed_to_comment = not pr_closed
423 426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
424 427
425 428 c.forbid_adding_reviewers = False
426 429
427 430 if pull_request_latest.reviewer_data and \
428 431 'rules' in pull_request_latest.reviewer_data:
429 432 rules = pull_request_latest.reviewer_data['rules'] or {}
430 433 try:
431 434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 435 except Exception:
433 436 pass
434 437
435 438 # check merge capabilities
436 439 _merge_check = MergeCheck.validate(
437 440 pull_request_latest, auth_user=self._rhodecode_user,
438 441 translator=self.request.translate,
439 442 force_shadow_repo_refresh=force_refresh)
440 443
441 444 c.pr_merge_errors = _merge_check.error_details
442 445 c.pr_merge_possible = not _merge_check.failed
443 446 c.pr_merge_message = _merge_check.merge_msg
444 447 c.pr_merge_source_commit = _merge_check.source_commit
445 448 c.pr_merge_target_commit = _merge_check.target_commit
446 449
447 450 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 451 pull_request_latest, translator=self.request.translate)
449 452
450 453 c.pull_request_review_status = _merge_check.review_status
451 454 if merge_checks:
452 455 self.request.override_renderer = \
453 456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 457 return self._get_template_context(c)
455 458
456 459 c.reviewers_count = pull_request.reviewers_count
457 460 c.observers_count = pull_request.observers_count
458 461
459 462 # reviewers and statuses
460 463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
462 465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
463 466
464 467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
465 468 member_reviewer = h.reviewer_as_json(
466 469 member, reasons=reasons, mandatory=mandatory,
467 470 role=review_obj.role,
468 471 user_group=review_obj.rule_user_group_data()
469 472 )
470 473
471 474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
472 475 member_reviewer['review_status'] = current_review_status
473 476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
474 477 member_reviewer['allowed_to_update'] = c.allowed_to_update
475 478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
476 479
477 480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
478 481
479 482 for observer_obj, member in pull_request_at_ver.observers():
480 483 member_observer = h.reviewer_as_json(
481 484 member, reasons=[], mandatory=False,
482 485 role=observer_obj.role,
483 486 user_group=observer_obj.rule_user_group_data()
484 487 )
485 488 member_observer['allowed_to_update'] = c.allowed_to_update
486 489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
487 490
488 491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
489 492
490 493 general_comments, inline_comments = \
491 494 self.register_comments_vars(c, pull_request_latest, versions)
492 495
493 496 # TODOs
494 497 c.unresolved_comments = CommentsModel() \
495 498 .get_pull_request_unresolved_todos(pull_request_latest)
496 499 c.resolved_comments = CommentsModel() \
497 500 .get_pull_request_resolved_todos(pull_request_latest)
498 501
499 502 # Drafts
500 503 c.draft_comments = CommentsModel().get_pull_request_drafts(
501 504 self._rhodecode_db_user.user_id,
502 505 pull_request_latest)
503 506
504 507 # if we use version, then do not show later comments
505 508 # than current version
506 509 display_inline_comments = collections.defaultdict(
507 510 lambda: collections.defaultdict(list))
508 511 for co in inline_comments:
509 512 if c.at_version_num:
510 513 # pick comments that are at least UPTO given version, so we
511 514 # don't render comments for higher version
512 515 should_render = co.pull_request_version_id and \
513 516 co.pull_request_version_id <= c.at_version_num
514 517 else:
515 518 # showing all, for 'latest'
516 519 should_render = True
517 520
518 521 if should_render:
519 522 display_inline_comments[co.f_path][co.line_no].append(co)
520 523
521 524 # load diff data into template context, if we use compare mode then
522 525 # diff is calculated based on changes between versions of PR
523 526
524 527 source_repo = pull_request_at_ver.source_repo
525 528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
526 529
527 530 target_repo = pull_request_at_ver.target_repo
528 531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
529 532
530 533 if compare:
531 534 # in compare switch the diff base to latest commit from prev version
532 535 target_ref_id = prev_pull_request_display_obj.revisions[0]
533 536
534 537 # despite opening commits for bookmarks/branches/tags, we always
535 538 # convert this to rev to prevent changes after bookmark or branch change
536 539 c.source_ref_type = 'rev'
537 540 c.source_ref = source_ref_id
538 541
539 542 c.target_ref_type = 'rev'
540 543 c.target_ref = target_ref_id
541 544
542 545 c.source_repo = source_repo
543 546 c.target_repo = target_repo
544 547
545 548 c.commit_ranges = []
546 549 source_commit = EmptyCommit()
547 550 target_commit = EmptyCommit()
548 551 c.missing_requirements = False
549 552
550 553 source_scm = source_repo.scm_instance()
551 554 target_scm = target_repo.scm_instance()
552 555
553 556 shadow_scm = None
554 557 try:
555 558 shadow_scm = pull_request_latest.get_shadow_repo()
556 559 except Exception:
557 560 log.debug('Failed to get shadow repo', exc_info=True)
558 561 # try first the existing source_repo, and then shadow
559 562 # repo if we can obtain one
560 563 commits_source_repo = source_scm
561 564 if shadow_scm:
562 565 commits_source_repo = shadow_scm
563 566
564 567 c.commits_source_repo = commits_source_repo
565 568 c.ancestor = None # set it to None, to hide it from PR view
566 569
567 570 # empty version means latest, so we keep this to prevent
568 571 # double caching
569 572 version_normalized = version or PullRequest.LATEST_VER
570 573 from_version_normalized = from_version or PullRequest.LATEST_VER
571 574
572 575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
573 576 cache_file_path = diff_cache_exist(
574 577 cache_path, 'pull_request', pull_request_id, version_normalized,
575 578 from_version_normalized, source_ref_id, target_ref_id,
576 579 hide_whitespace_changes, diff_context, c.fulldiff)
577 580
578 581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
579 582 force_recache = self.get_recache_flag()
580 583
581 584 cached_diff = None
582 585 if caching_enabled:
583 586 cached_diff = load_cached_diff(cache_file_path)
584 587
585 588 has_proper_commit_cache = (
586 589 cached_diff and cached_diff.get('commits')
587 590 and len(cached_diff.get('commits', [])) == 5
588 591 and cached_diff.get('commits')[0]
589 592 and cached_diff.get('commits')[3])
590 593
591 594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
592 595 diff_commit_cache = \
593 596 (ancestor_commit, commit_cache, missing_requirements,
594 597 source_commit, target_commit) = cached_diff['commits']
595 598 else:
596 599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
597 600 # merge errors resulting in potentially hidden commits in the shadow repo.
598 601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
599 602 and _merge_check.merge_response
600 603 maybe_unreachable = maybe_unreachable \
601 604 and _merge_check.merge_response.metadata.get('unresolved_files')
602 605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
603 606 diff_commit_cache = \
604 607 (ancestor_commit, commit_cache, missing_requirements,
605 608 source_commit, target_commit) = self.get_commits(
606 609 commits_source_repo,
607 610 pull_request_at_ver,
608 611 source_commit,
609 612 source_ref_id,
610 613 source_scm,
611 614 target_commit,
612 615 target_ref_id,
613 616 target_scm,
614 617 maybe_unreachable=maybe_unreachable)
615 618
616 619 # register our commit range
617 620 for comm in commit_cache.values():
618 621 c.commit_ranges.append(comm)
619 622
620 623 c.missing_requirements = missing_requirements
621 624 c.ancestor_commit = ancestor_commit
622 625 c.statuses = source_repo.statuses(
623 626 [x.raw_id for x in c.commit_ranges])
624 627
625 628 # auto collapse if we have more than limit
626 629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
627 630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
628 631 c.compare_mode = compare
629 632
630 633 # diff_limit is the old behavior, will cut off the whole diff
631 634 # if the limit is applied otherwise will just hide the
632 635 # big files from the front-end
633 636 diff_limit = c.visual.cut_off_limit_diff
634 637 file_limit = c.visual.cut_off_limit_file
635 638
636 639 c.missing_commits = False
637 640 if (c.missing_requirements
638 641 or isinstance(source_commit, EmptyCommit)
639 642 or source_commit == target_commit):
640 643
641 644 c.missing_commits = True
642 645 else:
643 646 c.inline_comments = display_inline_comments
644 647
645 648 use_ancestor = True
646 649 if from_version_normalized != version_normalized:
647 650 use_ancestor = False
648 651
649 652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
650 653 if not force_recache and has_proper_diff_cache:
651 654 c.diffset = cached_diff['diff']
652 655 else:
653 656 try:
654 657 c.diffset = self._get_diffset(
655 658 c.source_repo.repo_name, commits_source_repo,
656 659 c.ancestor_commit,
657 660 source_ref_id, target_ref_id,
658 661 target_commit, source_commit,
659 662 diff_limit, file_limit, c.fulldiff,
660 663 hide_whitespace_changes, diff_context,
661 664 use_ancestor=use_ancestor
662 665 )
663 666
664 667 # save cached diff
665 668 if caching_enabled:
666 669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
667 670 except CommitDoesNotExistError:
668 671 log.exception('Failed to generate diffset')
669 672 c.missing_commits = True
670 673
671 674 if not c.missing_commits:
672 675
673 676 c.limited_diff = c.diffset.limited_diff
674 677
675 678 # calculate removed files that are bound to comments
676 679 comment_deleted_files = [
677 680 fname for fname in display_inline_comments
678 681 if fname not in c.diffset.file_stats]
679 682
680 683 c.deleted_files_comments = collections.defaultdict(dict)
681 684 for fname, per_line_comments in display_inline_comments.items():
682 685 if fname in comment_deleted_files:
683 686 c.deleted_files_comments[fname]['stats'] = 0
684 687 c.deleted_files_comments[fname]['comments'] = list()
685 688 for lno, comments in per_line_comments.items():
686 689 c.deleted_files_comments[fname]['comments'].extend(comments)
687 690
688 691 # maybe calculate the range diff
689 692 if c.range_diff_on:
690 693 # TODO(marcink): set whitespace/context
691 694 context_lcl = 3
692 695 ign_whitespace_lcl = False
693 696
694 697 for commit in c.commit_ranges:
695 698 commit2 = commit
696 699 commit1 = commit.first_parent
697 700
698 701 range_diff_cache_file_path = diff_cache_exist(
699 702 cache_path, 'diff', commit.raw_id,
700 703 ign_whitespace_lcl, context_lcl, c.fulldiff)
701 704
702 705 cached_diff = None
703 706 if caching_enabled:
704 707 cached_diff = load_cached_diff(range_diff_cache_file_path)
705 708
706 709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
707 710 if not force_recache and has_proper_diff_cache:
708 711 diffset = cached_diff['diff']
709 712 else:
710 713 diffset = self._get_range_diffset(
711 714 commits_source_repo, source_repo,
712 715 commit1, commit2, diff_limit, file_limit,
713 716 c.fulldiff, ign_whitespace_lcl, context_lcl
714 717 )
715 718
716 719 # save cached diff
717 720 if caching_enabled:
718 721 cache_diff(range_diff_cache_file_path, diffset, None)
719 722
720 723 c.changes[commit.raw_id] = diffset
721 724
722 725 # this is a hack to properly display links, when creating PR, the
723 726 # compare view and others uses different notation, and
724 727 # compare_commits.mako renders links based on the target_repo.
725 728 # We need to swap that here to generate it properly on the html side
726 729 c.target_repo = c.source_repo
727 730
728 731 c.commit_statuses = ChangesetStatus.STATUSES
729 732
730 733 c.show_version_changes = not pr_closed
731 734 if c.show_version_changes:
732 735 cur_obj = pull_request_at_ver
733 736 prev_obj = prev_pull_request_at_ver
734 737
735 738 old_commit_ids = prev_obj.revisions
736 739 new_commit_ids = cur_obj.revisions
737 740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
738 741 old_commit_ids, new_commit_ids)
739 742 c.commit_changes_summary = commit_changes
740 743
741 744 # calculate the diff for commits between versions
742 745 c.commit_changes = []
743 746
744 747 def mark(cs, fw):
745 748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
746 749
747 750 for c_type, raw_id in mark(commit_changes.added, 'a') \
748 751 + mark(commit_changes.removed, 'r') \
749 752 + mark(commit_changes.common, 'c'):
750 753
751 754 if raw_id in commit_cache:
752 755 commit = commit_cache[raw_id]
753 756 else:
754 757 try:
755 758 commit = commits_source_repo.get_commit(raw_id)
756 759 except CommitDoesNotExistError:
757 760 # in case we fail extracting still use "dummy" commit
758 761 # for display in commit diff
759 762 commit = h.AttributeDict(
760 763 {'raw_id': raw_id,
761 764 'message': 'EMPTY or MISSING COMMIT'})
762 765 c.commit_changes.append([c_type, commit])
763 766
764 767 # current user review statuses for each version
765 768 c.review_versions = {}
766 769 is_reviewer = PullRequestModel().is_user_reviewer(
767 770 pull_request, self._rhodecode_user)
768 771 if is_reviewer:
769 772 for co in general_comments:
770 773 if co.author.user_id == self._rhodecode_user.user_id:
771 774 status = co.status_change
772 775 if status:
773 776 _ver_pr = status[0].comment.pull_request_version_id
774 777 c.review_versions[_ver_pr] = status[0]
775 778
776 779 return self._get_template_context(c)
777 780
778 781 def get_commits(
779 782 self, commits_source_repo, pull_request_at_ver, source_commit,
780 783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
781 784 maybe_unreachable=False):
782 785
783 786 commit_cache = collections.OrderedDict()
784 787 missing_requirements = False
785 788
786 789 try:
787 790 pre_load = ["author", "date", "message", "branch", "parents"]
788 791
789 792 pull_request_commits = pull_request_at_ver.revisions
790 793 log.debug('Loading %s commits from %s',
791 794 len(pull_request_commits), commits_source_repo)
792 795
793 796 for rev in pull_request_commits:
794 797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
795 798 maybe_unreachable=maybe_unreachable)
796 799 commit_cache[comm.raw_id] = comm
797 800
798 801 # Order here matters, we first need to get target, and then
799 802 # the source
800 803 target_commit = commits_source_repo.get_commit(
801 804 commit_id=safe_str(target_ref_id))
802 805
803 806 source_commit = commits_source_repo.get_commit(
804 807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
805 808 except CommitDoesNotExistError:
806 809 log.warning('Failed to get commit from `{}` repo'.format(
807 810 commits_source_repo), exc_info=True)
808 811 except RepositoryRequirementError:
809 812 log.warning('Failed to get all required data from repo', exc_info=True)
810 813 missing_requirements = True
811 814
812 815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
813 816
814 817 try:
815 818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
816 819 except Exception:
817 820 ancestor_commit = None
818 821
819 822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
820 823
821 824 def assure_not_empty_repo(self):
822 825 _ = self.request.translate
823 826
824 827 try:
825 828 self.db_repo.scm_instance().get_commit()
826 829 except EmptyRepositoryError:
827 830 h.flash(h.literal(_('There are no commits yet')),
828 831 category='warning')
829 832 raise HTTPFound(
830 833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
831 834
832 835 @LoginRequired()
833 836 @NotAnonymous()
834 837 @HasRepoPermissionAnyDecorator(
835 838 'repository.read', 'repository.write', 'repository.admin')
836 839 def pull_request_new(self):
837 840 _ = self.request.translate
838 841 c = self.load_default_context()
839 842
840 843 self.assure_not_empty_repo()
841 844 source_repo = self.db_repo
842 845
843 846 commit_id = self.request.GET.get('commit')
844 847 branch_ref = self.request.GET.get('branch')
845 848 bookmark_ref = self.request.GET.get('bookmark')
846 849
847 850 try:
848 851 source_repo_data = PullRequestModel().generate_repo_data(
849 852 source_repo, commit_id=commit_id,
850 853 branch=branch_ref, bookmark=bookmark_ref,
851 854 translator=self.request.translate)
852 855 except CommitDoesNotExistError as e:
853 856 log.exception(e)
854 857 h.flash(_('Commit does not exist'), 'error')
855 858 raise HTTPFound(
856 859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
857 860
858 861 default_target_repo = source_repo
859 862
860 863 if source_repo.parent and c.has_origin_repo_read_perm:
861 864 parent_vcs_obj = source_repo.parent.scm_instance()
862 865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
863 866 # change default if we have a parent repo
864 867 default_target_repo = source_repo.parent
865 868
866 869 target_repo_data = PullRequestModel().generate_repo_data(
867 870 default_target_repo, translator=self.request.translate)
868 871
869 872 selected_source_ref = source_repo_data['refs']['selected_ref']
870 873 title_source_ref = ''
871 874 if selected_source_ref:
872 875 title_source_ref = selected_source_ref.split(':', 2)[1]
873 876 c.default_title = PullRequestModel().generate_pullrequest_title(
874 877 source=source_repo.repo_name,
875 878 source_ref=title_source_ref,
876 879 target=default_target_repo.repo_name
877 880 )
878 881
879 882 c.default_repo_data = {
880 883 'source_repo_name': source_repo.repo_name,
881 884 'source_refs_json': json.dumps(source_repo_data),
882 885 'target_repo_name': default_target_repo.repo_name,
883 886 'target_refs_json': json.dumps(target_repo_data),
884 887 }
885 888 c.default_source_ref = selected_source_ref
886 889
887 890 return self._get_template_context(c)
888 891
889 892 @LoginRequired()
890 893 @NotAnonymous()
891 894 @HasRepoPermissionAnyDecorator(
892 895 'repository.read', 'repository.write', 'repository.admin')
893 896 def pull_request_repo_refs(self):
894 897 self.load_default_context()
895 898 target_repo_name = self.request.matchdict['target_repo_name']
896 899 repo = Repository.get_by_repo_name(target_repo_name)
897 900 if not repo:
898 901 raise HTTPNotFound()
899 902
900 903 target_perm = HasRepoPermissionAny(
901 904 'repository.read', 'repository.write', 'repository.admin')(
902 905 target_repo_name)
903 906 if not target_perm:
904 907 raise HTTPNotFound()
905 908
906 909 return PullRequestModel().generate_repo_data(
907 910 repo, translator=self.request.translate)
908 911
909 912 @LoginRequired()
910 913 @NotAnonymous()
911 914 @HasRepoPermissionAnyDecorator(
912 915 'repository.read', 'repository.write', 'repository.admin')
913 916 def pullrequest_repo_targets(self):
914 917 _ = self.request.translate
915 918 filter_query = self.request.GET.get('query')
916 919
917 920 # get the parents
918 921 parent_target_repos = []
919 922 if self.db_repo.parent:
920 923 parents_query = Repository.query() \
921 924 .order_by(func.length(Repository.repo_name)) \
922 925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
923 926
924 927 if filter_query:
925 928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
926 929 parents_query = parents_query.filter(
927 930 Repository.repo_name.ilike(ilike_expression))
928 931 parents = parents_query.limit(20).all()
929 932
930 933 for parent in parents:
931 934 parent_vcs_obj = parent.scm_instance()
932 935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
933 936 parent_target_repos.append(parent)
934 937
935 938 # get other forks, and repo itself
936 939 query = Repository.query() \
937 940 .order_by(func.length(Repository.repo_name)) \
938 941 .filter(
939 942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
940 943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
941 944 ) \
942 945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
943 946
944 947 if filter_query:
945 948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
946 949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
947 950
948 951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
949 952 target_repos = query.limit(limit).all()
950 953
951 954 all_target_repos = target_repos + parent_target_repos
952 955
953 956 repos = []
954 957 # This checks permissions to the repositories
955 958 for obj in ScmModel().get_repos(all_target_repos):
956 959 repos.append({
957 960 'id': obj['name'],
958 961 'text': obj['name'],
959 962 'type': 'repo',
960 963 'repo_id': obj['dbrepo']['repo_id'],
961 964 'repo_type': obj['dbrepo']['repo_type'],
962 965 'private': obj['dbrepo']['private'],
963 966
964 967 })
965 968
966 969 data = {
967 970 'more': False,
968 971 'results': [{
969 972 'text': _('Repositories'),
970 973 'children': repos
971 974 }] if repos else []
972 975 }
973 976 return data
974 977
975 978 @classmethod
976 979 def get_comment_ids(cls, post_data):
977 980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
978 981
979 982 @LoginRequired()
980 983 @NotAnonymous()
981 984 @HasRepoPermissionAnyDecorator(
982 985 'repository.read', 'repository.write', 'repository.admin')
983 986 def pullrequest_comments(self):
984 987 self.load_default_context()
985 988
986 989 pull_request = PullRequest.get_or_404(
987 990 self.request.matchdict['pull_request_id'])
988 991 pull_request_id = pull_request.pull_request_id
989 992 version = self.request.GET.get('version')
990 993
991 994 _render = self.request.get_partial_renderer(
992 995 'rhodecode:templates/base/sidebar.mako')
993 996 c = _render.get_call_context()
994 997
995 998 (pull_request_latest,
996 999 pull_request_at_ver,
997 1000 pull_request_display_obj,
998 1001 at_version) = PullRequestModel().get_pr_version(
999 1002 pull_request_id, version=version)
1000 1003 versions = pull_request_display_obj.versions()
1001 1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1002 1005 c.versions = versions + [latest_ver]
1003 1006
1004 1007 c.at_version = at_version
1005 1008 c.at_version_num = (at_version
1006 1009 if at_version and at_version != PullRequest.LATEST_VER
1007 1010 else None)
1008 1011
1009 1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1010 1013 all_comments = c.inline_comments_flat + c.comments
1011 1014
1012 1015 existing_ids = self.get_comment_ids(self.request.POST)
1013 1016 return _render('comments_table', all_comments, len(all_comments),
1014 1017 existing_ids=existing_ids)
1015 1018
1016 1019 @LoginRequired()
1017 1020 @NotAnonymous()
1018 1021 @HasRepoPermissionAnyDecorator(
1019 1022 'repository.read', 'repository.write', 'repository.admin')
1020 1023 def pullrequest_todos(self):
1021 1024 self.load_default_context()
1022 1025
1023 1026 pull_request = PullRequest.get_or_404(
1024 1027 self.request.matchdict['pull_request_id'])
1025 1028 pull_request_id = pull_request.pull_request_id
1026 1029 version = self.request.GET.get('version')
1027 1030
1028 1031 _render = self.request.get_partial_renderer(
1029 1032 'rhodecode:templates/base/sidebar.mako')
1030 1033 c = _render.get_call_context()
1031 1034 (pull_request_latest,
1032 1035 pull_request_at_ver,
1033 1036 pull_request_display_obj,
1034 1037 at_version) = PullRequestModel().get_pr_version(
1035 1038 pull_request_id, version=version)
1036 1039 versions = pull_request_display_obj.versions()
1037 1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1038 1041 c.versions = versions + [latest_ver]
1039 1042
1040 1043 c.at_version = at_version
1041 1044 c.at_version_num = (at_version
1042 1045 if at_version and at_version != PullRequest.LATEST_VER
1043 1046 else None)
1044 1047
1045 1048 c.unresolved_comments = CommentsModel() \
1046 1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1047 1050 c.resolved_comments = CommentsModel() \
1048 1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1049 1052
1050 1053 all_comments = c.unresolved_comments + c.resolved_comments
1051 1054 existing_ids = self.get_comment_ids(self.request.POST)
1052 1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1053 1056 todo_comments=True, existing_ids=existing_ids)
1054 1057
1055 1058 @LoginRequired()
1056 1059 @NotAnonymous()
1057 1060 @HasRepoPermissionAnyDecorator(
1058 1061 'repository.read', 'repository.write', 'repository.admin')
1059 1062 def pullrequest_drafts(self):
1060 1063 self.load_default_context()
1061 1064
1062 1065 pull_request = PullRequest.get_or_404(
1063 1066 self.request.matchdict['pull_request_id'])
1064 1067 pull_request_id = pull_request.pull_request_id
1065 1068 version = self.request.GET.get('version')
1066 1069
1067 1070 _render = self.request.get_partial_renderer(
1068 1071 'rhodecode:templates/base/sidebar.mako')
1069 1072 c = _render.get_call_context()
1070 1073
1071 1074 (pull_request_latest,
1072 1075 pull_request_at_ver,
1073 1076 pull_request_display_obj,
1074 1077 at_version) = PullRequestModel().get_pr_version(
1075 1078 pull_request_id, version=version)
1076 1079 versions = pull_request_display_obj.versions()
1077 1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1078 1081 c.versions = versions + [latest_ver]
1079 1082
1080 1083 c.at_version = at_version
1081 1084 c.at_version_num = (at_version
1082 1085 if at_version and at_version != PullRequest.LATEST_VER
1083 1086 else None)
1084 1087
1085 1088 c.draft_comments = CommentsModel() \
1086 1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1087 1090
1088 1091 all_comments = c.draft_comments
1089 1092
1090 1093 existing_ids = self.get_comment_ids(self.request.POST)
1091 1094 return _render('comments_table', all_comments, len(all_comments),
1092 1095 existing_ids=existing_ids, draft_comments=True)
1093 1096
1094 1097 @LoginRequired()
1095 1098 @NotAnonymous()
1096 1099 @HasRepoPermissionAnyDecorator(
1097 1100 'repository.read', 'repository.write', 'repository.admin')
1098 1101 @CSRFRequired()
1099 1102 def pull_request_create(self):
1100 1103 _ = self.request.translate
1101 1104 self.assure_not_empty_repo()
1102 1105 self.load_default_context()
1103 1106
1104 1107 controls = peppercorn.parse(self.request.POST.items())
1105 1108
1106 1109 try:
1107 1110 form = PullRequestForm(
1108 1111 self.request.translate, self.db_repo.repo_id)()
1109 1112 _form = form.to_python(controls)
1110 1113 except formencode.Invalid as errors:
1111 1114 if errors.error_dict.get('revisions'):
1112 1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1113 1116 elif errors.error_dict.get('pullrequest_title'):
1114 1117 msg = errors.error_dict.get('pullrequest_title')
1115 1118 else:
1116 1119 msg = _('Error creating pull request: {}').format(errors)
1117 1120 log.exception(msg)
1118 1121 h.flash(msg, 'error')
1119 1122
1120 1123 # would rather just go back to form ...
1121 1124 raise HTTPFound(
1122 1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1123 1126
1124 1127 source_repo = _form['source_repo']
1125 1128 source_ref = _form['source_ref']
1126 1129 target_repo = _form['target_repo']
1127 1130 target_ref = _form['target_ref']
1128 1131 commit_ids = _form['revisions'][::-1]
1129 1132 common_ancestor_id = _form['common_ancestor']
1130 1133
1131 1134 # find the ancestor for this pr
1132 1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1133 1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1134 1137
1135 1138 if not (source_db_repo or target_db_repo):
1136 1139 h.flash(_('source_repo or target repo not found'), category='error')
1137 1140 raise HTTPFound(
1138 1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1139 1142
1140 1143 # re-check permissions again here
1141 1144 # source_repo we must have read permissions
1142 1145
1143 1146 source_perm = HasRepoPermissionAny(
1144 1147 'repository.read', 'repository.write', 'repository.admin')(
1145 1148 source_db_repo.repo_name)
1146 1149 if not source_perm:
1147 1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1148 1151 source_db_repo.repo_name))
1149 1152 h.flash(msg, category='error')
1150 1153 # copy the args back to redirect
1151 1154 org_query = self.request.GET.mixed()
1152 1155 raise HTTPFound(
1153 1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1154 1157 _query=org_query))
1155 1158
1156 1159 # target repo we must have read permissions, and also later on
1157 1160 # we want to check branch permissions here
1158 1161 target_perm = HasRepoPermissionAny(
1159 1162 'repository.read', 'repository.write', 'repository.admin')(
1160 1163 target_db_repo.repo_name)
1161 1164 if not target_perm:
1162 1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1163 1166 target_db_repo.repo_name))
1164 1167 h.flash(msg, category='error')
1165 1168 # copy the args back to redirect
1166 1169 org_query = self.request.GET.mixed()
1167 1170 raise HTTPFound(
1168 1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1169 1172 _query=org_query))
1170 1173
1171 1174 source_scm = source_db_repo.scm_instance()
1172 1175 target_scm = target_db_repo.scm_instance()
1173 1176
1174 1177 source_ref_obj = unicode_to_reference(source_ref)
1175 1178 target_ref_obj = unicode_to_reference(target_ref)
1176 1179
1177 1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1178 1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1179 1182
1180 1183 ancestor = source_scm.get_common_ancestor(
1181 1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1182 1185
1183 1186 # recalculate target ref based on ancestor
1184 1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1185 1188
1186 1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1187 1190 PullRequestModel().get_reviewer_functions()
1188 1191
1189 1192 # recalculate reviewers logic, to make sure we can validate this
1190 1193 reviewer_rules = get_default_reviewers_data(
1191 1194 self._rhodecode_db_user,
1192 1195 source_db_repo,
1193 1196 source_ref_obj,
1194 1197 target_db_repo,
1195 1198 target_ref_obj,
1196 1199 include_diff_info=False)
1197 1200
1198 1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1199 1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1200 1203
1201 1204 pullrequest_title = _form['pullrequest_title']
1202 1205 title_source_ref = source_ref_obj.name
1203 1206 if not pullrequest_title:
1204 1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1205 1208 source=source_repo,
1206 1209 source_ref=title_source_ref,
1207 1210 target=target_repo
1208 1211 )
1209 1212
1210 1213 description = _form['pullrequest_desc']
1211 1214 description_renderer = _form['description_renderer']
1212 1215
1213 1216 try:
1214 1217 pull_request = PullRequestModel().create(
1215 1218 created_by=self._rhodecode_user.user_id,
1216 1219 source_repo=source_repo,
1217 1220 source_ref=source_ref,
1218 1221 target_repo=target_repo,
1219 1222 target_ref=target_ref,
1220 1223 revisions=commit_ids,
1221 1224 common_ancestor_id=common_ancestor_id,
1222 1225 reviewers=reviewers,
1223 1226 observers=observers,
1224 1227 title=pullrequest_title,
1225 1228 description=description,
1226 1229 description_renderer=description_renderer,
1227 1230 reviewer_data=reviewer_rules,
1228 1231 auth_user=self._rhodecode_user
1229 1232 )
1230 1233 Session().commit()
1231 1234
1232 1235 h.flash(_('Successfully opened new pull request'),
1233 1236 category='success')
1234 1237 except Exception:
1235 1238 msg = _('Error occurred during creation of this pull request.')
1236 1239 log.exception(msg)
1237 1240 h.flash(msg, category='error')
1238 1241
1239 1242 # copy the args back to redirect
1240 1243 org_query = self.request.GET.mixed()
1241 1244 raise HTTPFound(
1242 1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1243 1246 _query=org_query))
1244 1247
1245 1248 raise HTTPFound(
1246 1249 h.route_path('pullrequest_show', repo_name=target_repo,
1247 1250 pull_request_id=pull_request.pull_request_id))
1248 1251
1249 1252 @LoginRequired()
1250 1253 @NotAnonymous()
1251 1254 @HasRepoPermissionAnyDecorator(
1252 1255 'repository.read', 'repository.write', 'repository.admin')
1253 1256 @CSRFRequired()
1254 1257 def pull_request_update(self):
1255 1258 pull_request = PullRequest.get_or_404(
1256 1259 self.request.matchdict['pull_request_id'])
1257 1260 _ = self.request.translate
1258 1261
1259 1262 c = self.load_default_context()
1260 1263 redirect_url = None
1261 1264
1262 1265 if pull_request.is_closed():
1263 1266 log.debug('update: forbidden because pull request is closed')
1264 1267 msg = _(u'Cannot update closed pull requests.')
1265 1268 h.flash(msg, category='error')
1266 1269 return {'response': True,
1267 1270 'redirect_url': redirect_url}
1268 1271
1269 1272 is_state_changing = pull_request.is_state_changing()
1270 1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1271 1274
1272 1275 # only owner or admin can update it
1273 1276 allowed_to_update = PullRequestModel().check_user_update(
1274 1277 pull_request, self._rhodecode_user)
1275 1278
1276 1279 if allowed_to_update:
1277 1280 controls = peppercorn.parse(self.request.POST.items())
1278 1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1279 1282
1280 1283 if 'review_members' in controls:
1281 1284 self._update_reviewers(
1282 1285 c,
1283 1286 pull_request, controls['review_members'],
1284 1287 pull_request.reviewer_data,
1285 1288 PullRequestReviewers.ROLE_REVIEWER)
1286 1289 elif 'observer_members' in controls:
1287 1290 self._update_reviewers(
1288 1291 c,
1289 1292 pull_request, controls['observer_members'],
1290 1293 pull_request.reviewer_data,
1291 1294 PullRequestReviewers.ROLE_OBSERVER)
1292 1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1293 1296 if is_state_changing:
1294 1297 log.debug('commits update: forbidden because pull request is in state %s',
1295 1298 pull_request.pull_request_state)
1296 1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1297 1300 u'Current state is: `{}`').format(
1298 1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1299 1302 h.flash(msg, category='error')
1300 1303 return {'response': True,
1301 1304 'redirect_url': redirect_url}
1302 1305
1303 1306 self._update_commits(c, pull_request)
1304 1307 if force_refresh:
1305 1308 redirect_url = h.route_path(
1306 1309 'pullrequest_show', repo_name=self.db_repo_name,
1307 1310 pull_request_id=pull_request.pull_request_id,
1308 1311 _query={"force_refresh": 1})
1309 1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1310 1313 self._edit_pull_request(pull_request)
1311 1314 else:
1312 1315 log.error('Unhandled update data.')
1313 1316 raise HTTPBadRequest()
1314 1317
1315 1318 return {'response': True,
1316 1319 'redirect_url': redirect_url}
1317 1320 raise HTTPForbidden()
1318 1321
1319 1322 def _edit_pull_request(self, pull_request):
1320 1323 """
1321 1324 Edit title and description
1322 1325 """
1323 1326 _ = self.request.translate
1324 1327
1325 1328 try:
1326 1329 PullRequestModel().edit(
1327 1330 pull_request,
1328 1331 self.request.POST.get('title'),
1329 1332 self.request.POST.get('description'),
1330 1333 self.request.POST.get('description_renderer'),
1331 1334 self._rhodecode_user)
1332 1335 except ValueError:
1333 1336 msg = _(u'Cannot update closed pull requests.')
1334 1337 h.flash(msg, category='error')
1335 1338 return
1336 1339 else:
1337 1340 Session().commit()
1338 1341
1339 1342 msg = _(u'Pull request title & description updated.')
1340 1343 h.flash(msg, category='success')
1341 1344 return
1342 1345
1343 1346 def _update_commits(self, c, pull_request):
1344 1347 _ = self.request.translate
1345 1348
1346 1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1347 1350 resp = PullRequestModel().update_commits(
1348 1351 pull_request, self._rhodecode_db_user)
1349 1352
1350 1353 if resp.executed:
1351 1354
1352 1355 if resp.target_changed and resp.source_changed:
1353 1356 changed = 'target and source repositories'
1354 1357 elif resp.target_changed and not resp.source_changed:
1355 1358 changed = 'target repository'
1356 1359 elif not resp.target_changed and resp.source_changed:
1357 1360 changed = 'source repository'
1358 1361 else:
1359 1362 changed = 'nothing'
1360 1363
1361 1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1362 1365 u'{count_added} added, {count_removed} removed commits. '
1363 1366 u'Source of changes: {change_source}.')
1364 1367 msg = msg.format(
1365 1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1366 1369 count_added=len(resp.changes.added),
1367 1370 count_removed=len(resp.changes.removed),
1368 1371 change_source=changed)
1369 1372 h.flash(msg, category='success')
1370 1373 channelstream.pr_update_channelstream_push(
1371 1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1372 1375 else:
1373 1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1374 1377 warning_reasons = [
1375 1378 UpdateFailureReason.NO_CHANGE,
1376 1379 UpdateFailureReason.WRONG_REF_TYPE,
1377 1380 ]
1378 1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1379 1382 h.flash(msg, category=category)
1380 1383
1381 1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1382 1385 _ = self.request.translate
1383 1386
1384 1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1385 1388 PullRequestModel().get_reviewer_functions()
1386 1389
1387 1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1388 1391 try:
1389 1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1390 1393 except ValueError as e:
1391 1394 log.error('Reviewers Validation: {}'.format(e))
1392 1395 h.flash(e, category='error')
1393 1396 return
1394 1397
1395 1398 old_calculated_status = pull_request.calculated_review_status()
1396 1399 PullRequestModel().update_reviewers(
1397 1400 pull_request, reviewers, self._rhodecode_db_user)
1398 1401
1399 1402 Session().commit()
1400 1403
1401 1404 msg = _('Pull request reviewers updated.')
1402 1405 h.flash(msg, category='success')
1403 1406 channelstream.pr_update_channelstream_push(
1404 1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1405 1408
1406 1409 # trigger status changed if change in reviewers changes the status
1407 1410 calculated_status = pull_request.calculated_review_status()
1408 1411 if old_calculated_status != calculated_status:
1409 1412 PullRequestModel().trigger_pull_request_hook(
1410 1413 pull_request, self._rhodecode_user, 'review_status_change',
1411 1414 data={'status': calculated_status})
1412 1415
1413 1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1414 1417 try:
1415 1418 observers = validate_observers(review_members, reviewer_rules)
1416 1419 except ValueError as e:
1417 1420 log.error('Observers Validation: {}'.format(e))
1418 1421 h.flash(e, category='error')
1419 1422 return
1420 1423
1421 1424 PullRequestModel().update_observers(
1422 1425 pull_request, observers, self._rhodecode_db_user)
1423 1426
1424 1427 Session().commit()
1425 1428 msg = _('Pull request observers updated.')
1426 1429 h.flash(msg, category='success')
1427 1430 channelstream.pr_update_channelstream_push(
1428 1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1429 1432
1430 1433 @LoginRequired()
1431 1434 @NotAnonymous()
1432 1435 @HasRepoPermissionAnyDecorator(
1433 1436 'repository.read', 'repository.write', 'repository.admin')
1434 1437 @CSRFRequired()
1435 1438 def pull_request_merge(self):
1436 1439 """
1437 1440 Merge will perform a server-side merge of the specified
1438 1441 pull request, if the pull request is approved and mergeable.
1439 1442 After successful merging, the pull request is automatically
1440 1443 closed, with a relevant comment.
1441 1444 """
1442 1445 pull_request = PullRequest.get_or_404(
1443 1446 self.request.matchdict['pull_request_id'])
1444 1447 _ = self.request.translate
1445 1448
1446 1449 if pull_request.is_state_changing():
1447 1450 log.debug('show: forbidden because pull request is in state %s',
1448 1451 pull_request.pull_request_state)
1449 1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1450 1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1451 1454 pull_request.pull_request_state)
1452 1455 h.flash(msg, category='error')
1453 1456 raise HTTPFound(
1454 1457 h.route_path('pullrequest_show',
1455 1458 repo_name=pull_request.target_repo.repo_name,
1456 1459 pull_request_id=pull_request.pull_request_id))
1457 1460
1458 1461 self.load_default_context()
1459 1462
1460 1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1461 1464 check = MergeCheck.validate(
1462 1465 pull_request, auth_user=self._rhodecode_user,
1463 1466 translator=self.request.translate)
1464 1467 merge_possible = not check.failed
1465 1468
1466 1469 for err_type, error_msg in check.errors:
1467 1470 h.flash(error_msg, category=err_type)
1468 1471
1469 1472 if merge_possible:
1470 1473 log.debug("Pre-conditions checked, trying to merge.")
1471 1474 extras = vcs_operation_context(
1472 1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1473 1476 username=self._rhodecode_db_user.username, action='push',
1474 1477 scm=pull_request.target_repo.repo_type)
1475 1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1476 1479 self._merge_pull_request(
1477 1480 pull_request, self._rhodecode_db_user, extras)
1478 1481 else:
1479 1482 log.debug("Pre-conditions failed, NOT merging.")
1480 1483
1481 1484 raise HTTPFound(
1482 1485 h.route_path('pullrequest_show',
1483 1486 repo_name=pull_request.target_repo.repo_name,
1484 1487 pull_request_id=pull_request.pull_request_id))
1485 1488
1486 1489 def _merge_pull_request(self, pull_request, user, extras):
1487 1490 _ = self.request.translate
1488 1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1489 1492
1490 1493 if merge_resp.executed:
1491 1494 log.debug("The merge was successful, closing the pull request.")
1492 1495 PullRequestModel().close_pull_request(
1493 1496 pull_request.pull_request_id, user)
1494 1497 Session().commit()
1495 1498 msg = _('Pull request was successfully merged and closed.')
1496 1499 h.flash(msg, category='success')
1497 1500 else:
1498 1501 log.debug(
1499 1502 "The merge was not successful. Merge response: %s", merge_resp)
1500 1503 msg = merge_resp.merge_status_message
1501 1504 h.flash(msg, category='error')
1502 1505
1503 1506 @LoginRequired()
1504 1507 @NotAnonymous()
1505 1508 @HasRepoPermissionAnyDecorator(
1506 1509 'repository.read', 'repository.write', 'repository.admin')
1507 1510 @CSRFRequired()
1508 1511 def pull_request_delete(self):
1509 1512 _ = self.request.translate
1510 1513
1511 1514 pull_request = PullRequest.get_or_404(
1512 1515 self.request.matchdict['pull_request_id'])
1513 1516 self.load_default_context()
1514 1517
1515 1518 pr_closed = pull_request.is_closed()
1516 1519 allowed_to_delete = PullRequestModel().check_user_delete(
1517 1520 pull_request, self._rhodecode_user) and not pr_closed
1518 1521
1519 1522 # only owner can delete it !
1520 1523 if allowed_to_delete:
1521 1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1522 1525 Session().commit()
1523 1526 h.flash(_('Successfully deleted pull request'),
1524 1527 category='success')
1525 1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1526 1529 repo_name=self.db_repo_name))
1527 1530
1528 1531 log.warning('user %s tried to delete pull request without access',
1529 1532 self._rhodecode_user)
1530 1533 raise HTTPNotFound()
1531 1534
1532 1535 def _pull_request_comments_create(self, pull_request, comments):
1533 1536 _ = self.request.translate
1534 1537 data = {}
1535 1538 if not comments:
1536 1539 return
1537 1540 pull_request_id = pull_request.pull_request_id
1538 1541
1539 1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1540 1543
1541 1544 for entry in comments:
1542 1545 c = self.load_default_context()
1543 1546 comment_type = entry['comment_type']
1544 1547 text = entry['text']
1545 1548 status = entry['status']
1546 1549 is_draft = str2bool(entry['is_draft'])
1547 1550 resolves_comment_id = entry['resolves_comment_id']
1548 1551 close_pull_request = entry['close_pull_request']
1549 1552 f_path = entry['f_path']
1550 1553 line_no = entry['line']
1551 1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1552 1555
1553 1556 # the logic here should work like following, if we submit close
1554 1557 # pr comment, use `close_pull_request_with_comment` function
1555 1558 # else handle regular comment logic
1556 1559
1557 1560 if close_pull_request:
1558 1561 # only owner or admin or person with write permissions
1559 1562 allowed_to_close = PullRequestModel().check_user_update(
1560 1563 pull_request, self._rhodecode_user)
1561 1564 if not allowed_to_close:
1562 1565 log.debug('comment: forbidden because not allowed to close '
1563 1566 'pull request %s', pull_request_id)
1564 1567 raise HTTPForbidden()
1565 1568
1566 1569 # This also triggers `review_status_change`
1567 1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1568 1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1569 1572 auth_user=self._rhodecode_user)
1570 1573 Session().flush()
1571 1574 is_inline = comment.is_inline
1572 1575
1573 1576 PullRequestModel().trigger_pull_request_hook(
1574 1577 pull_request, self._rhodecode_user, 'comment',
1575 1578 data={'comment': comment})
1576 1579
1577 1580 else:
1578 1581 # regular comment case, could be inline, or one with status.
1579 1582 # for that one we check also permissions
1580 1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1581 1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1582 1585 pull_request, self._rhodecode_user) and not is_draft
1583 1586
1584 1587 if status and allowed_to_change_status:
1585 1588 message = (_('Status change %(transition_icon)s %(status)s')
1586 1589 % {'transition_icon': '>',
1587 1590 'status': ChangesetStatus.get_status_lbl(status)})
1588 1591 text = text or message
1589 1592
1590 1593 comment = CommentsModel().create(
1591 1594 text=text,
1592 1595 repo=self.db_repo.repo_id,
1593 1596 user=self._rhodecode_user.user_id,
1594 1597 pull_request=pull_request,
1595 1598 f_path=f_path,
1596 1599 line_no=line_no,
1597 1600 status_change=(ChangesetStatus.get_status_lbl(status)
1598 1601 if status and allowed_to_change_status else None),
1599 1602 status_change_type=(status
1600 1603 if status and allowed_to_change_status else None),
1601 1604 comment_type=comment_type,
1602 1605 is_draft=is_draft,
1603 1606 resolves_comment_id=resolves_comment_id,
1604 1607 auth_user=self._rhodecode_user,
1605 1608 send_email=not is_draft, # skip notification for draft comments
1606 1609 )
1607 1610 is_inline = comment.is_inline
1608 1611
1609 1612 if allowed_to_change_status:
1610 1613 # calculate old status before we change it
1611 1614 old_calculated_status = pull_request.calculated_review_status()
1612 1615
1613 1616 # get status if set !
1614 1617 if status:
1615 1618 ChangesetStatusModel().set_status(
1616 1619 self.db_repo.repo_id,
1617 1620 status,
1618 1621 self._rhodecode_user.user_id,
1619 1622 comment,
1620 1623 pull_request=pull_request
1621 1624 )
1622 1625
1623 1626 Session().flush()
1624 1627 # this is somehow required to get access to some relationship
1625 1628 # loaded on comment
1626 1629 Session().refresh(comment)
1627 1630
1628 1631 # skip notifications for drafts
1629 1632 if not is_draft:
1630 1633 PullRequestModel().trigger_pull_request_hook(
1631 1634 pull_request, self._rhodecode_user, 'comment',
1632 1635 data={'comment': comment})
1633 1636
1634 1637 # we now calculate the status of pull request, and based on that
1635 1638 # calculation we set the commits status
1636 1639 calculated_status = pull_request.calculated_review_status()
1637 1640 if old_calculated_status != calculated_status:
1638 1641 PullRequestModel().trigger_pull_request_hook(
1639 1642 pull_request, self._rhodecode_user, 'review_status_change',
1640 1643 data={'status': calculated_status})
1641 1644
1642 1645 comment_id = comment.comment_id
1643 1646 data[comment_id] = {
1644 1647 'target_id': target_elem_id
1645 1648 }
1646 1649 Session().flush()
1647 1650
1648 1651 c.co = comment
1649 1652 c.at_version_num = None
1650 1653 c.is_new = True
1651 1654 rendered_comment = render(
1652 1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1653 1656 self._get_template_context(c), self.request)
1654 1657
1655 1658 data[comment_id].update(comment.get_dict())
1656 1659 data[comment_id].update({'rendered_text': rendered_comment})
1657 1660
1658 1661 Session().commit()
1659 1662
1660 1663 # skip channelstream for draft comments
1661 1664 if not all_drafts:
1662 1665 comment_broadcast_channel = channelstream.comment_channel(
1663 1666 self.db_repo_name, pull_request_obj=pull_request)
1664 1667
1665 1668 comment_data = data
1666 1669 posted_comment_type = 'inline' if is_inline else 'general'
1667 1670 if len(data) == 1:
1668 1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1669 1672 else:
1670 1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1671 1674
1672 1675 channelstream.comment_channelstream_push(
1673 1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1674 1677 comment_data=comment_data)
1675 1678
1676 1679 return data
1677 1680
1678 1681 @LoginRequired()
1679 1682 @NotAnonymous()
1680 1683 @HasRepoPermissionAnyDecorator(
1681 1684 'repository.read', 'repository.write', 'repository.admin')
1682 1685 @CSRFRequired()
1683 1686 def pull_request_comment_create(self):
1684 1687 _ = self.request.translate
1685 1688
1686 1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1687 1690
1688 1691 if pull_request.is_closed():
1689 1692 log.debug('comment: forbidden because pull request is closed')
1690 1693 raise HTTPForbidden()
1691 1694
1692 1695 allowed_to_comment = PullRequestModel().check_user_comment(
1693 1696 pull_request, self._rhodecode_user)
1694 1697 if not allowed_to_comment:
1695 1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1696 1699 raise HTTPForbidden()
1697 1700
1698 1701 comment_data = {
1699 1702 'comment_type': self.request.POST.get('comment_type'),
1700 1703 'text': self.request.POST.get('text'),
1701 1704 'status': self.request.POST.get('changeset_status', None),
1702 1705 'is_draft': self.request.POST.get('draft'),
1703 1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1704 1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1705 1708 'f_path': self.request.POST.get('f_path'),
1706 1709 'line': self.request.POST.get('line'),
1707 1710 }
1708 1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1709 1712
1710 1713 return data
1711 1714
1712 1715 @LoginRequired()
1713 1716 @NotAnonymous()
1714 1717 @HasRepoPermissionAnyDecorator(
1715 1718 'repository.read', 'repository.write', 'repository.admin')
1716 1719 @CSRFRequired()
1717 1720 def pull_request_comment_delete(self):
1718 1721 pull_request = PullRequest.get_or_404(
1719 1722 self.request.matchdict['pull_request_id'])
1720 1723
1721 1724 comment = ChangesetComment.get_or_404(
1722 1725 self.request.matchdict['comment_id'])
1723 1726 comment_id = comment.comment_id
1724 1727
1725 1728 if comment.immutable:
1726 1729 # don't allow deleting comments that are immutable
1727 1730 raise HTTPForbidden()
1728 1731
1729 1732 if pull_request.is_closed():
1730 1733 log.debug('comment: forbidden because pull request is closed')
1731 1734 raise HTTPForbidden()
1732 1735
1733 1736 if not comment:
1734 1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1735 1738 # comment already deleted in another call probably
1736 1739 return True
1737 1740
1738 1741 if comment.pull_request.is_closed():
1739 1742 # don't allow deleting comments on closed pull request
1740 1743 raise HTTPForbidden()
1741 1744
1742 1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1743 1746 super_admin = h.HasPermissionAny('hg.admin')()
1744 1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1745 1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1746 1749 comment_repo_admin = is_repo_admin and is_repo_comment
1747 1750
1748 1751 if super_admin or comment_owner or comment_repo_admin:
1749 1752 old_calculated_status = comment.pull_request.calculated_review_status()
1750 1753 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1751 1754 Session().commit()
1752 1755 calculated_status = comment.pull_request.calculated_review_status()
1753 1756 if old_calculated_status != calculated_status:
1754 1757 PullRequestModel().trigger_pull_request_hook(
1755 1758 comment.pull_request, self._rhodecode_user, 'review_status_change',
1756 1759 data={'status': calculated_status})
1757 1760 return True
1758 1761 else:
1759 1762 log.warning('No permissions for user %s to delete comment_id: %s',
1760 1763 self._rhodecode_db_user, comment_id)
1761 1764 raise HTTPNotFound()
1762 1765
1763 1766 @LoginRequired()
1764 1767 @NotAnonymous()
1765 1768 @HasRepoPermissionAnyDecorator(
1766 1769 'repository.read', 'repository.write', 'repository.admin')
1767 1770 @CSRFRequired()
1768 1771 def pull_request_comment_edit(self):
1769 1772 self.load_default_context()
1770 1773
1771 1774 pull_request = PullRequest.get_or_404(
1772 1775 self.request.matchdict['pull_request_id']
1773 1776 )
1774 1777 comment = ChangesetComment.get_or_404(
1775 1778 self.request.matchdict['comment_id']
1776 1779 )
1777 1780 comment_id = comment.comment_id
1778 1781
1779 1782 if comment.immutable:
1780 1783 # don't allow deleting comments that are immutable
1781 1784 raise HTTPForbidden()
1782 1785
1783 1786 if pull_request.is_closed():
1784 1787 log.debug('comment: forbidden because pull request is closed')
1785 1788 raise HTTPForbidden()
1786 1789
1787 1790 if comment.pull_request.is_closed():
1788 1791 # don't allow deleting comments on closed pull request
1789 1792 raise HTTPForbidden()
1790 1793
1791 1794 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1792 1795 super_admin = h.HasPermissionAny('hg.admin')()
1793 1796 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1794 1797 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1795 1798 comment_repo_admin = is_repo_admin and is_repo_comment
1796 1799
1797 1800 if super_admin or comment_owner or comment_repo_admin:
1798 1801 text = self.request.POST.get('text')
1799 1802 version = self.request.POST.get('version')
1800 1803 if text == comment.text:
1801 1804 log.warning(
1802 1805 'Comment(PR): '
1803 1806 'Trying to create new version '
1804 1807 'with the same comment body {}'.format(
1805 1808 comment_id,
1806 1809 )
1807 1810 )
1808 1811 raise HTTPNotFound()
1809 1812
1810 1813 if version.isdigit():
1811 1814 version = int(version)
1812 1815 else:
1813 1816 log.warning(
1814 1817 'Comment(PR): Wrong version type {} {} '
1815 1818 'for comment {}'.format(
1816 1819 version,
1817 1820 type(version),
1818 1821 comment_id,
1819 1822 )
1820 1823 )
1821 1824 raise HTTPNotFound()
1822 1825
1823 1826 try:
1824 1827 comment_history = CommentsModel().edit(
1825 1828 comment_id=comment_id,
1826 1829 text=text,
1827 1830 auth_user=self._rhodecode_user,
1828 1831 version=version,
1829 1832 )
1830 1833 except CommentVersionMismatch:
1831 1834 raise HTTPConflict()
1832 1835
1833 1836 if not comment_history:
1834 1837 raise HTTPNotFound()
1835 1838
1836 1839 Session().commit()
1837 1840 if not comment.draft:
1838 1841 PullRequestModel().trigger_pull_request_hook(
1839 1842 pull_request, self._rhodecode_user, 'comment_edit',
1840 1843 data={'comment': comment})
1841 1844
1842 1845 return {
1843 1846 'comment_history_id': comment_history.comment_history_id,
1844 1847 'comment_id': comment.comment_id,
1845 1848 'comment_version': comment_history.version,
1846 1849 'comment_author_username': comment_history.author.username,
1847 1850 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1848 1851 'comment_created_on': h.age_component(comment_history.created_on,
1849 1852 time_is_local=True),
1850 1853 }
1851 1854 else:
1852 1855 log.warning('No permissions for user %s to edit comment_id: %s',
1853 1856 self._rhodecode_db_user, comment_id)
1854 1857 raise HTTPNotFound()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2233 +1,2247 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 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 os
30 30
31 31 import datetime
32 32 import urllib
33 33 import collections
34 34
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.translation import lazy_ugettext
40 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 41 from rhodecode.lib import audit_logger
42 42 from rhodecode.lib.compat import OrderedDict
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.markup_renderer import (
45 45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 46 from rhodecode.lib.utils2 import (
47 47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 48 get_current_rhodecode_user)
49 49 from rhodecode.lib.vcs.backends.base import (
50 50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 51 TargetRefMissing, SourceRefMissing)
52 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 CommitDoesNotExistError, EmptyRepositoryError)
55 55 from rhodecode.model import BaseModel
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (
59 59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.notification import NotificationModel, \
63 63 EmailNotificationModel
64 64 from rhodecode.model.scm import ScmModel
65 65 from rhodecode.model.settings import VcsSettingsModel
66 66
67 67
68 68 log = logging.getLogger(__name__)
69 69
70 70
71 71 # Data structure to hold the response data when updating commits during a pull
72 72 # request update.
73 73 class UpdateResponse(object):
74 74
75 75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 76 commit_changes, source_changed, target_changed):
77 77
78 78 self.executed = executed
79 79 self.reason = reason
80 80 self.new = new
81 81 self.old = old
82 82 self.common_ancestor_id = common_ancestor_id
83 83 self.changes = commit_changes
84 84 self.source_changed = source_changed
85 85 self.target_changed = target_changed
86 86
87 87
88 88 def get_diff_info(
89 89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 90 get_commit_authors=True):
91 91 """
92 92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 93 This is also used for default reviewers logic
94 94 """
95 95
96 96 source_scm = source_repo.scm_instance()
97 97 target_scm = target_repo.scm_instance()
98 98
99 99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 100 if not ancestor_id:
101 101 raise ValueError(
102 102 'cannot calculate diff info without a common ancestor. '
103 103 'Make sure both repositories are related, and have a common forking commit.')
104 104
105 105 # case here is that want a simple diff without incoming commits,
106 106 # previewing what will be merged based only on commits in the source.
107 107 log.debug('Using ancestor %s as source_ref instead of %s',
108 108 ancestor_id, source_ref)
109 109
110 110 # source of changes now is the common ancestor
111 111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 112 # target commit becomes the source ref as it is the last commit
113 113 # for diff generation this logic gives proper diff
114 114 target_commit = source_scm.get_commit(commit_id=source_ref)
115 115
116 116 vcs_diff = \
117 117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 118 ignore_whitespace=False, context=3)
119 119
120 120 diff_processor = diffs.DiffProcessor(
121 121 vcs_diff, format='newdiff', diff_limit=None,
122 122 file_limit=None, show_full_diff=True)
123 123
124 124 _parsed = diff_processor.prepare()
125 125
126 126 all_files = []
127 127 all_files_changes = []
128 128 changed_lines = {}
129 129 stats = [0, 0]
130 130 for f in _parsed:
131 131 all_files.append(f['filename'])
132 132 all_files_changes.append({
133 133 'filename': f['filename'],
134 134 'stats': f['stats']
135 135 })
136 136 stats[0] += f['stats']['added']
137 137 stats[1] += f['stats']['deleted']
138 138
139 139 changed_lines[f['filename']] = []
140 140 if len(f['chunks']) < 2:
141 141 continue
142 142 # first line is "context" information
143 143 for chunks in f['chunks'][1:]:
144 144 for chunk in chunks['lines']:
145 145 if chunk['action'] not in ('del', 'mod'):
146 146 continue
147 147 changed_lines[f['filename']].append(chunk['old_lineno'])
148 148
149 149 commit_authors = []
150 150 user_counts = {}
151 151 email_counts = {}
152 152 author_counts = {}
153 153 _commit_cache = {}
154 154
155 155 commits = []
156 156 if get_commit_authors:
157 157 log.debug('Obtaining commit authors from set of commits')
158 158 _compare_data = target_scm.compare(
159 159 target_ref, source_ref, source_scm, merge=True,
160 160 pre_load=["author", "date", "message"]
161 161 )
162 162
163 163 for commit in _compare_data:
164 164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 165 # at this function which is later called via JSON serialization
166 166 serialized_commit = dict(
167 167 author=commit.author,
168 168 date=commit.date,
169 169 message=commit.message,
170 170 commit_id=commit.raw_id,
171 171 raw_id=commit.raw_id
172 172 )
173 173 commits.append(serialized_commit)
174 174 user = User.get_from_cs_author(serialized_commit['author'])
175 175 if user and user not in commit_authors:
176 176 commit_authors.append(user)
177 177
178 178 # lines
179 179 if get_authors:
180 180 log.debug('Calculating authors of changed files')
181 181 target_commit = source_repo.get_commit(ancestor_id)
182 182
183 183 for fname, lines in changed_lines.items():
184 184
185 185 try:
186 186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 187 except Exception:
188 188 log.exception("Failed to load node with path %s", fname)
189 189 continue
190 190
191 191 if not isinstance(node, FileNode):
192 192 continue
193 193
194 194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 195 if node.is_binary:
196 196 author = node.last_commit.author
197 197 email = node.last_commit.author_email
198 198
199 199 user = User.get_from_cs_author(author)
200 200 if user:
201 201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 202 author_counts[author] = author_counts.get(author, 0) + 1
203 203 email_counts[email] = email_counts.get(email, 0) + 1
204 204
205 205 continue
206 206
207 207 for annotation in node.annotate:
208 208 line_no, commit_id, get_commit_func, line_text = annotation
209 209 if line_no in lines:
210 210 if commit_id not in _commit_cache:
211 211 _commit_cache[commit_id] = get_commit_func()
212 212 commit = _commit_cache[commit_id]
213 213 author = commit.author
214 214 email = commit.author_email
215 215 user = User.get_from_cs_author(author)
216 216 if user:
217 217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 218 author_counts[author] = author_counts.get(author, 0) + 1
219 219 email_counts[email] = email_counts.get(email, 0) + 1
220 220
221 221 log.debug('Default reviewers processing finished')
222 222
223 223 return {
224 224 'commits': commits,
225 225 'files': all_files_changes,
226 226 'stats': stats,
227 227 'ancestor': ancestor_id,
228 228 # original authors of modified files
229 229 'original_authors': {
230 230 'users': user_counts,
231 231 'authors': author_counts,
232 232 'emails': email_counts,
233 233 },
234 234 'commit_authors': commit_authors
235 235 }
236 236
237 237
238 238 class PullRequestModel(BaseModel):
239 239
240 240 cls = PullRequest
241 241
242 242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243 243
244 244 UPDATE_STATUS_MESSAGES = {
245 245 UpdateFailureReason.NONE: lazy_ugettext(
246 246 'Pull request update successful.'),
247 247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 248 'Pull request update failed because of an unknown error.'),
249 249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 250 'No update needed because the source and target have not changed.'),
251 251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 252 'Pull request cannot be updated because the reference type is '
253 253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 255 'This pull request cannot be updated because the target '
256 256 'reference is missing.'),
257 257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 258 'This pull request cannot be updated because the source '
259 259 'reference is missing.'),
260 260 }
261 261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263 263
264 264 def __get_pull_request(self, pull_request):
265 265 return self._get_instance((
266 266 PullRequest, PullRequestVersion), pull_request)
267 267
268 268 def _check_perms(self, perms, pull_request, user, api=False):
269 269 if not api:
270 270 return h.HasRepoPermissionAny(*perms)(
271 271 user=user, repo_name=pull_request.target_repo.repo_name)
272 272 else:
273 273 return h.HasRepoPermissionAnyApi(*perms)(
274 274 user=user, repo_name=pull_request.target_repo.repo_name)
275 275
276 276 def check_user_read(self, pull_request, user, api=False):
277 277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 278 return self._check_perms(_perms, pull_request, user, api)
279 279
280 280 def check_user_merge(self, pull_request, user, api=False):
281 281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 282 return self._check_perms(_perms, pull_request, user, api)
283 283
284 284 def check_user_update(self, pull_request, user, api=False):
285 285 owner = user.user_id == pull_request.user_id
286 286 return self.check_user_merge(pull_request, user, api) or owner
287 287
288 288 def check_user_delete(self, pull_request, user):
289 289 owner = user.user_id == pull_request.user_id
290 290 _perms = ('repository.admin',)
291 291 return self._check_perms(_perms, pull_request, user) or owner
292 292
293 293 def is_user_reviewer(self, pull_request, user):
294 294 return user.user_id in [
295 295 x.user_id for x in
296 296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 297 if x.user
298 298 ]
299 299
300 300 def check_user_change_status(self, pull_request, user, api=False):
301 301 return self.check_user_update(pull_request, user, api) \
302 302 or self.is_user_reviewer(pull_request, user)
303 303
304 304 def check_user_comment(self, pull_request, user):
305 305 owner = user.user_id == pull_request.user_id
306 306 return self.check_user_read(pull_request, user) or owner
307 307
308 308 def get(self, pull_request):
309 309 return self.__get_pull_request(pull_request)
310 310
311 311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 312 statuses=None, opened_by=None, order_by=None,
313 313 order_dir='desc', only_created=False):
314 314 repo = None
315 315 if repo_name:
316 316 repo = self._get_repo(repo_name)
317 317
318 318 q = PullRequest.query()
319 319
320 320 if search_q:
321 321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 322 q = q.join(User)
323 323 q = q.filter(or_(
324 324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 325 User.username.ilike(like_expression),
326 326 PullRequest.title.ilike(like_expression),
327 327 PullRequest.description.ilike(like_expression),
328 328 ))
329 329
330 330 # source or target
331 331 if repo and source:
332 332 q = q.filter(PullRequest.source_repo == repo)
333 333 elif repo:
334 334 q = q.filter(PullRequest.target_repo == repo)
335 335
336 336 # closed,opened
337 337 if statuses:
338 338 q = q.filter(PullRequest.status.in_(statuses))
339 339
340 340 # opened by filter
341 341 if opened_by:
342 342 q = q.filter(PullRequest.user_id.in_(opened_by))
343 343
344 344 # only get those that are in "created" state
345 345 if only_created:
346 346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347 347
348 348 if order_by:
349 349 order_map = {
350 350 'name_raw': PullRequest.pull_request_id,
351 351 'id': PullRequest.pull_request_id,
352 352 'title': PullRequest.title,
353 353 'updated_on_raw': PullRequest.updated_on,
354 354 'target_repo': PullRequest.target_repo_id
355 355 }
356 356 if order_dir == 'asc':
357 357 q = q.order_by(order_map[order_by].asc())
358 358 else:
359 359 q = q.order_by(order_map[order_by].desc())
360 360
361 361 return q
362 362
363 363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 364 opened_by=None):
365 365 """
366 366 Count the number of pull requests for a specific repository.
367 367
368 368 :param repo_name: target or source repo
369 369 :param search_q: filter by text
370 370 :param source: boolean flag to specify if repo_name refers to source
371 371 :param statuses: list of pull request statuses
372 372 :param opened_by: author user of the pull request
373 373 :returns: int number of pull requests
374 374 """
375 375 q = self._prepare_get_all_query(
376 376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 377 opened_by=opened_by)
378 378
379 379 return q.count()
380 380
381 381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 383 """
384 384 Get all pull requests for a specific repository.
385 385
386 386 :param repo_name: target or source repo
387 387 :param search_q: filter by text
388 388 :param source: boolean flag to specify if repo_name refers to source
389 389 :param statuses: list of pull request statuses
390 390 :param opened_by: author user of the pull request
391 391 :param offset: pagination offset
392 392 :param length: length of returned list
393 393 :param order_by: order of the returned list
394 394 :param order_dir: 'asc' or 'desc' ordering direction
395 395 :returns: list of pull requests
396 396 """
397 397 q = self._prepare_get_all_query(
398 398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400 400
401 401 if length:
402 402 pull_requests = q.limit(length).offset(offset).all()
403 403 else:
404 404 pull_requests = q.all()
405 405
406 406 return pull_requests
407 407
408 408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
409 409 opened_by=None):
410 410 """
411 411 Count the number of pull requests for a specific repository that are
412 412 awaiting review.
413 413
414 414 :param repo_name: target or source repo
415 415 :param search_q: filter by text
416 416 :param source: boolean flag to specify if repo_name refers to source
417 417 :param statuses: list of pull request statuses
418 418 :param opened_by: author user of the pull request
419 419 :returns: int number of pull requests
420 420 """
421 421 pull_requests = self.get_awaiting_review(
422 422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
423 423
424 424 return len(pull_requests)
425 425
426 426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
427 427 opened_by=None, offset=0, length=None,
428 428 order_by=None, order_dir='desc'):
429 429 """
430 430 Get all pull requests for a specific repository that are awaiting
431 431 review.
432 432
433 433 :param repo_name: target or source repo
434 434 :param search_q: filter by text
435 435 :param source: boolean flag to specify if repo_name refers to source
436 436 :param statuses: list of pull request statuses
437 437 :param opened_by: author user of the pull request
438 438 :param offset: pagination offset
439 439 :param length: length of returned list
440 440 :param order_by: order of the returned list
441 441 :param order_dir: 'asc' or 'desc' ordering direction
442 442 :returns: list of pull requests
443 443 """
444 444 pull_requests = self.get_all(
445 445 repo_name, search_q=search_q, source=source, statuses=statuses,
446 446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
447 447
448 448 _filtered_pull_requests = []
449 449 for pr in pull_requests:
450 450 status = pr.calculated_review_status()
451 451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 452 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 453 _filtered_pull_requests.append(pr)
454 454 if length:
455 455 return _filtered_pull_requests[offset:offset+length]
456 456 else:
457 457 return _filtered_pull_requests
458 458
459 459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
460 460 opened_by=None, user_id=None):
461 461 """
462 462 Count the number of pull requests for a specific repository that are
463 463 awaiting review from a specific user.
464 464
465 465 :param repo_name: target or source repo
466 466 :param search_q: filter by text
467 467 :param source: boolean flag to specify if repo_name refers to source
468 468 :param statuses: list of pull request statuses
469 469 :param opened_by: author user of the pull request
470 470 :param user_id: reviewer user of the pull request
471 471 :returns: int number of pull requests
472 472 """
473 473 pull_requests = self.get_awaiting_my_review(
474 474 repo_name, search_q=search_q, source=source, statuses=statuses,
475 475 opened_by=opened_by, user_id=user_id)
476 476
477 477 return len(pull_requests)
478 478
479 479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 480 opened_by=None, user_id=None, offset=0,
481 481 length=None, order_by=None, order_dir='desc'):
482 482 """
483 483 Get all pull requests for a specific repository that are awaiting
484 484 review from a specific user.
485 485
486 486 :param repo_name: target or source repo
487 487 :param search_q: filter by text
488 488 :param source: boolean flag to specify if repo_name refers to source
489 489 :param statuses: list of pull request statuses
490 490 :param opened_by: author user of the pull request
491 491 :param user_id: reviewer user of the pull request
492 492 :param offset: pagination offset
493 493 :param length: length of returned list
494 494 :param order_by: order of the returned list
495 495 :param order_dir: 'asc' or 'desc' ordering direction
496 496 :returns: list of pull requests
497 497 """
498 498 pull_requests = self.get_all(
499 499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501 501
502 502 _my = PullRequestModel().get_not_reviewed(user_id)
503 503 my_participation = []
504 504 for pr in pull_requests:
505 505 if pr in _my:
506 506 my_participation.append(pr)
507 507 _filtered_pull_requests = my_participation
508 508 if length:
509 509 return _filtered_pull_requests[offset:offset+length]
510 510 else:
511 511 return _filtered_pull_requests
512 512
513 513 def get_not_reviewed(self, user_id):
514 514 return [
515 515 x.pull_request for x in PullRequestReviewers.query().filter(
516 516 PullRequestReviewers.user_id == user_id).all()
517 517 ]
518 518
519 519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 520 order_by=None, order_dir='desc'):
521 521 q = PullRequest.query()
522 522 if user_id:
523 523 reviewers_subquery = Session().query(
524 524 PullRequestReviewers.pull_request_id).filter(
525 525 PullRequestReviewers.user_id == user_id).subquery()
526 526 user_filter = or_(
527 527 PullRequest.user_id == user_id,
528 528 PullRequest.pull_request_id.in_(reviewers_subquery)
529 529 )
530 530 q = PullRequest.query().filter(user_filter)
531 531
532 532 # closed,opened
533 533 if statuses:
534 534 q = q.filter(PullRequest.status.in_(statuses))
535 535
536 536 if query:
537 537 like_expression = u'%{}%'.format(safe_unicode(query))
538 538 q = q.join(User)
539 539 q = q.filter(or_(
540 540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 541 User.username.ilike(like_expression),
542 542 PullRequest.title.ilike(like_expression),
543 543 PullRequest.description.ilike(like_expression),
544 544 ))
545 545 if order_by:
546 546 order_map = {
547 547 'name_raw': PullRequest.pull_request_id,
548 548 'title': PullRequest.title,
549 549 'updated_on_raw': PullRequest.updated_on,
550 550 'target_repo': PullRequest.target_repo_id
551 551 }
552 552 if order_dir == 'asc':
553 553 q = q.order_by(order_map[order_by].asc())
554 554 else:
555 555 q = q.order_by(order_map[order_by].desc())
556 556
557 557 return q
558 558
559 559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
561 561 return q.count()
562 562
563 563 def get_im_participating_in(
564 564 self, user_id=None, statuses=None, query='', offset=0,
565 565 length=None, order_by=None, order_dir='desc'):
566 566 """
567 567 Get all Pull requests that i'm participating in, or i have opened
568 568 """
569 569
570 570 q = self._prepare_participating_query(
571 571 user_id, statuses=statuses, query=query, order_by=order_by,
572 572 order_dir=order_dir)
573 573
574 574 if length:
575 575 pull_requests = q.limit(length).offset(offset).all()
576 576 else:
577 577 pull_requests = q.all()
578 578
579 579 return pull_requests
580 580
581 581 def get_versions(self, pull_request):
582 582 """
583 583 returns version of pull request sorted by ID descending
584 584 """
585 585 return PullRequestVersion.query()\
586 586 .filter(PullRequestVersion.pull_request == pull_request)\
587 587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 588 .all()
589 589
590 590 def get_pr_version(self, pull_request_id, version=None):
591 591 at_version = None
592 592
593 593 if version and version == 'latest':
594 594 pull_request_ver = PullRequest.get(pull_request_id)
595 595 pull_request_obj = pull_request_ver
596 596 _org_pull_request_obj = pull_request_obj
597 597 at_version = 'latest'
598 598 elif version:
599 599 pull_request_ver = PullRequestVersion.get_or_404(version)
600 600 pull_request_obj = pull_request_ver
601 601 _org_pull_request_obj = pull_request_ver.pull_request
602 602 at_version = pull_request_ver.pull_request_version_id
603 603 else:
604 604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 605 pull_request_id)
606 606
607 607 pull_request_display_obj = PullRequest.get_pr_display_object(
608 608 pull_request_obj, _org_pull_request_obj)
609 609
610 610 return _org_pull_request_obj, pull_request_obj, \
611 611 pull_request_display_obj, at_version
612 612
613 def pr_commits_versions(self, versions):
614 """
615 Maps the pull-request commits into all known PR versions. This way we can obtain
616 each pr version the commit was introduced in.
617 """
618 commit_versions = collections.defaultdict(list)
619 num_versions = [x.pull_request_version_id for x in versions]
620 for ver in versions:
621 for commit_id in ver.revisions:
622 ver_idx = ChangesetComment.get_index_from_version(
623 ver.pull_request_version_id, num_versions=num_versions)
624 commit_versions[commit_id].append(ver_idx)
625 return commit_versions
626
613 627 def create(self, created_by, source_repo, source_ref, target_repo,
614 628 target_ref, revisions, reviewers, observers, title, description=None,
615 629 common_ancestor_id=None,
616 630 description_renderer=None,
617 631 reviewer_data=None, translator=None, auth_user=None):
618 632 translator = translator or get_current_request().translate
619 633
620 634 created_by_user = self._get_user(created_by)
621 635 auth_user = auth_user or created_by_user.AuthUser()
622 636 source_repo = self._get_repo(source_repo)
623 637 target_repo = self._get_repo(target_repo)
624 638
625 639 pull_request = PullRequest()
626 640 pull_request.source_repo = source_repo
627 641 pull_request.source_ref = source_ref
628 642 pull_request.target_repo = target_repo
629 643 pull_request.target_ref = target_ref
630 644 pull_request.revisions = revisions
631 645 pull_request.title = title
632 646 pull_request.description = description
633 647 pull_request.description_renderer = description_renderer
634 648 pull_request.author = created_by_user
635 649 pull_request.reviewer_data = reviewer_data
636 650 pull_request.pull_request_state = pull_request.STATE_CREATING
637 651 pull_request.common_ancestor_id = common_ancestor_id
638 652
639 653 Session().add(pull_request)
640 654 Session().flush()
641 655
642 656 reviewer_ids = set()
643 657 # members / reviewers
644 658 for reviewer_object in reviewers:
645 659 user_id, reasons, mandatory, role, rules = reviewer_object
646 660 user = self._get_user(user_id)
647 661
648 662 # skip duplicates
649 663 if user.user_id in reviewer_ids:
650 664 continue
651 665
652 666 reviewer_ids.add(user.user_id)
653 667
654 668 reviewer = PullRequestReviewers()
655 669 reviewer.user = user
656 670 reviewer.pull_request = pull_request
657 671 reviewer.reasons = reasons
658 672 reviewer.mandatory = mandatory
659 673 reviewer.role = role
660 674
661 675 # NOTE(marcink): pick only first rule for now
662 676 rule_id = list(rules)[0] if rules else None
663 677 rule = RepoReviewRule.get(rule_id) if rule_id else None
664 678 if rule:
665 679 review_group = rule.user_group_vote_rule(user_id)
666 680 # we check if this particular reviewer is member of a voting group
667 681 if review_group:
668 682 # NOTE(marcink):
669 683 # can be that user is member of more but we pick the first same,
670 684 # same as default reviewers algo
671 685 review_group = review_group[0]
672 686
673 687 rule_data = {
674 688 'rule_name':
675 689 rule.review_rule_name,
676 690 'rule_user_group_entry_id':
677 691 review_group.repo_review_rule_users_group_id,
678 692 'rule_user_group_name':
679 693 review_group.users_group.users_group_name,
680 694 'rule_user_group_members':
681 695 [x.user.username for x in review_group.users_group.members],
682 696 'rule_user_group_members_id':
683 697 [x.user.user_id for x in review_group.users_group.members],
684 698 }
685 699 # e.g {'vote_rule': -1, 'mandatory': True}
686 700 rule_data.update(review_group.rule_data())
687 701
688 702 reviewer.rule_data = rule_data
689 703
690 704 Session().add(reviewer)
691 705 Session().flush()
692 706
693 707 for observer_object in observers:
694 708 user_id, reasons, mandatory, role, rules = observer_object
695 709 user = self._get_user(user_id)
696 710
697 711 # skip duplicates from reviewers
698 712 if user.user_id in reviewer_ids:
699 713 continue
700 714
701 715 #reviewer_ids.add(user.user_id)
702 716
703 717 observer = PullRequestReviewers()
704 718 observer.user = user
705 719 observer.pull_request = pull_request
706 720 observer.reasons = reasons
707 721 observer.mandatory = mandatory
708 722 observer.role = role
709 723
710 724 # NOTE(marcink): pick only first rule for now
711 725 rule_id = list(rules)[0] if rules else None
712 726 rule = RepoReviewRule.get(rule_id) if rule_id else None
713 727 if rule:
714 728 # TODO(marcink): do we need this for observers ??
715 729 pass
716 730
717 731 Session().add(observer)
718 732 Session().flush()
719 733
720 734 # Set approval status to "Under Review" for all commits which are
721 735 # part of this pull request.
722 736 ChangesetStatusModel().set_status(
723 737 repo=target_repo,
724 738 status=ChangesetStatus.STATUS_UNDER_REVIEW,
725 739 user=created_by_user,
726 740 pull_request=pull_request
727 741 )
728 742 # we commit early at this point. This has to do with a fact
729 743 # that before queries do some row-locking. And because of that
730 744 # we need to commit and finish transaction before below validate call
731 745 # that for large repos could be long resulting in long row locks
732 746 Session().commit()
733 747
734 748 # prepare workspace, and run initial merge simulation. Set state during that
735 749 # operation
736 750 pull_request = PullRequest.get(pull_request.pull_request_id)
737 751
738 752 # set as merging, for merge simulation, and if finished to created so we mark
739 753 # simulation is working fine
740 754 with pull_request.set_state(PullRequest.STATE_MERGING,
741 755 final_state=PullRequest.STATE_CREATED) as state_obj:
742 756 MergeCheck.validate(
743 757 pull_request, auth_user=auth_user, translator=translator)
744 758
745 759 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
746 760 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
747 761
748 762 creation_data = pull_request.get_api_data(with_merge_state=False)
749 763 self._log_audit_action(
750 764 'repo.pull_request.create', {'data': creation_data},
751 765 auth_user, pull_request)
752 766
753 767 return pull_request
754 768
755 769 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
756 770 pull_request = self.__get_pull_request(pull_request)
757 771 target_scm = pull_request.target_repo.scm_instance()
758 772 if action == 'create':
759 773 trigger_hook = hooks_utils.trigger_create_pull_request_hook
760 774 elif action == 'merge':
761 775 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
762 776 elif action == 'close':
763 777 trigger_hook = hooks_utils.trigger_close_pull_request_hook
764 778 elif action == 'review_status_change':
765 779 trigger_hook = hooks_utils.trigger_review_pull_request_hook
766 780 elif action == 'update':
767 781 trigger_hook = hooks_utils.trigger_update_pull_request_hook
768 782 elif action == 'comment':
769 783 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
770 784 elif action == 'comment_edit':
771 785 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
772 786 else:
773 787 return
774 788
775 789 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
776 790 pull_request, action, trigger_hook)
777 791 trigger_hook(
778 792 username=user.username,
779 793 repo_name=pull_request.target_repo.repo_name,
780 794 repo_type=target_scm.alias,
781 795 pull_request=pull_request,
782 796 data=data)
783 797
784 798 def _get_commit_ids(self, pull_request):
785 799 """
786 800 Return the commit ids of the merged pull request.
787 801
788 802 This method is not dealing correctly yet with the lack of autoupdates
789 803 nor with the implicit target updates.
790 804 For example: if a commit in the source repo is already in the target it
791 805 will be reported anyways.
792 806 """
793 807 merge_rev = pull_request.merge_rev
794 808 if merge_rev is None:
795 809 raise ValueError('This pull request was not merged yet')
796 810
797 811 commit_ids = list(pull_request.revisions)
798 812 if merge_rev not in commit_ids:
799 813 commit_ids.append(merge_rev)
800 814
801 815 return commit_ids
802 816
803 817 def merge_repo(self, pull_request, user, extras):
804 818 log.debug("Merging pull request %s", pull_request.pull_request_id)
805 819 extras['user_agent'] = 'internal-merge'
806 820 merge_state = self._merge_pull_request(pull_request, user, extras)
807 821 if merge_state.executed:
808 822 log.debug("Merge was successful, updating the pull request comments.")
809 823 self._comment_and_close_pr(pull_request, user, merge_state)
810 824
811 825 self._log_audit_action(
812 826 'repo.pull_request.merge',
813 827 {'merge_state': merge_state.__dict__},
814 828 user, pull_request)
815 829
816 830 else:
817 831 log.warn("Merge failed, not updating the pull request.")
818 832 return merge_state
819 833
820 834 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
821 835 target_vcs = pull_request.target_repo.scm_instance()
822 836 source_vcs = pull_request.source_repo.scm_instance()
823 837
824 838 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
825 839 pr_id=pull_request.pull_request_id,
826 840 pr_title=pull_request.title,
827 841 source_repo=source_vcs.name,
828 842 source_ref_name=pull_request.source_ref_parts.name,
829 843 target_repo=target_vcs.name,
830 844 target_ref_name=pull_request.target_ref_parts.name,
831 845 )
832 846
833 847 workspace_id = self._workspace_id(pull_request)
834 848 repo_id = pull_request.target_repo.repo_id
835 849 use_rebase = self._use_rebase_for_merging(pull_request)
836 850 close_branch = self._close_branch_before_merging(pull_request)
837 851 user_name = self._user_name_for_merging(pull_request, user)
838 852
839 853 target_ref = self._refresh_reference(
840 854 pull_request.target_ref_parts, target_vcs)
841 855
842 856 callback_daemon, extras = prepare_callback_daemon(
843 857 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
844 858 host=vcs_settings.HOOKS_HOST,
845 859 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
846 860
847 861 with callback_daemon:
848 862 # TODO: johbo: Implement a clean way to run a config_override
849 863 # for a single call.
850 864 target_vcs.config.set(
851 865 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
852 866
853 867 merge_state = target_vcs.merge(
854 868 repo_id, workspace_id, target_ref, source_vcs,
855 869 pull_request.source_ref_parts,
856 870 user_name=user_name, user_email=user.email,
857 871 message=message, use_rebase=use_rebase,
858 872 close_branch=close_branch)
859 873 return merge_state
860 874
861 875 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
862 876 pull_request.merge_rev = merge_state.merge_ref.commit_id
863 877 pull_request.updated_on = datetime.datetime.now()
864 878 close_msg = close_msg or 'Pull request merged and closed'
865 879
866 880 CommentsModel().create(
867 881 text=safe_unicode(close_msg),
868 882 repo=pull_request.target_repo.repo_id,
869 883 user=user.user_id,
870 884 pull_request=pull_request.pull_request_id,
871 885 f_path=None,
872 886 line_no=None,
873 887 closing_pr=True
874 888 )
875 889
876 890 Session().add(pull_request)
877 891 Session().flush()
878 892 # TODO: paris: replace invalidation with less radical solution
879 893 ScmModel().mark_for_invalidation(
880 894 pull_request.target_repo.repo_name)
881 895 self.trigger_pull_request_hook(pull_request, user, 'merge')
882 896
883 897 def has_valid_update_type(self, pull_request):
884 898 source_ref_type = pull_request.source_ref_parts.type
885 899 return source_ref_type in self.REF_TYPES
886 900
887 901 def get_flow_commits(self, pull_request):
888 902
889 903 # source repo
890 904 source_ref_name = pull_request.source_ref_parts.name
891 905 source_ref_type = pull_request.source_ref_parts.type
892 906 source_ref_id = pull_request.source_ref_parts.commit_id
893 907 source_repo = pull_request.source_repo.scm_instance()
894 908
895 909 try:
896 910 if source_ref_type in self.REF_TYPES:
897 911 source_commit = source_repo.get_commit(source_ref_name)
898 912 else:
899 913 source_commit = source_repo.get_commit(source_ref_id)
900 914 except CommitDoesNotExistError:
901 915 raise SourceRefMissing()
902 916
903 917 # target repo
904 918 target_ref_name = pull_request.target_ref_parts.name
905 919 target_ref_type = pull_request.target_ref_parts.type
906 920 target_ref_id = pull_request.target_ref_parts.commit_id
907 921 target_repo = pull_request.target_repo.scm_instance()
908 922
909 923 try:
910 924 if target_ref_type in self.REF_TYPES:
911 925 target_commit = target_repo.get_commit(target_ref_name)
912 926 else:
913 927 target_commit = target_repo.get_commit(target_ref_id)
914 928 except CommitDoesNotExistError:
915 929 raise TargetRefMissing()
916 930
917 931 return source_commit, target_commit
918 932
919 933 def update_commits(self, pull_request, updating_user):
920 934 """
921 935 Get the updated list of commits for the pull request
922 936 and return the new pull request version and the list
923 937 of commits processed by this update action
924 938
925 939 updating_user is the user_object who triggered the update
926 940 """
927 941 pull_request = self.__get_pull_request(pull_request)
928 942 source_ref_type = pull_request.source_ref_parts.type
929 943 source_ref_name = pull_request.source_ref_parts.name
930 944 source_ref_id = pull_request.source_ref_parts.commit_id
931 945
932 946 target_ref_type = pull_request.target_ref_parts.type
933 947 target_ref_name = pull_request.target_ref_parts.name
934 948 target_ref_id = pull_request.target_ref_parts.commit_id
935 949
936 950 if not self.has_valid_update_type(pull_request):
937 951 log.debug("Skipping update of pull request %s due to ref type: %s",
938 952 pull_request, source_ref_type)
939 953 return UpdateResponse(
940 954 executed=False,
941 955 reason=UpdateFailureReason.WRONG_REF_TYPE,
942 956 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
943 957 source_changed=False, target_changed=False)
944 958
945 959 try:
946 960 source_commit, target_commit = self.get_flow_commits(pull_request)
947 961 except SourceRefMissing:
948 962 return UpdateResponse(
949 963 executed=False,
950 964 reason=UpdateFailureReason.MISSING_SOURCE_REF,
951 965 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
952 966 source_changed=False, target_changed=False)
953 967 except TargetRefMissing:
954 968 return UpdateResponse(
955 969 executed=False,
956 970 reason=UpdateFailureReason.MISSING_TARGET_REF,
957 971 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
958 972 source_changed=False, target_changed=False)
959 973
960 974 source_changed = source_ref_id != source_commit.raw_id
961 975 target_changed = target_ref_id != target_commit.raw_id
962 976
963 977 if not (source_changed or target_changed):
964 978 log.debug("Nothing changed in pull request %s", pull_request)
965 979 return UpdateResponse(
966 980 executed=False,
967 981 reason=UpdateFailureReason.NO_CHANGE,
968 982 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
969 983 source_changed=target_changed, target_changed=source_changed)
970 984
971 985 change_in_found = 'target repo' if target_changed else 'source repo'
972 986 log.debug('Updating pull request because of change in %s detected',
973 987 change_in_found)
974 988
975 989 # Finally there is a need for an update, in case of source change
976 990 # we create a new version, else just an update
977 991 if source_changed:
978 992 pull_request_version = self._create_version_from_snapshot(pull_request)
979 993 self._link_comments_to_version(pull_request_version)
980 994 else:
981 995 try:
982 996 ver = pull_request.versions[-1]
983 997 except IndexError:
984 998 ver = None
985 999
986 1000 pull_request.pull_request_version_id = \
987 1001 ver.pull_request_version_id if ver else None
988 1002 pull_request_version = pull_request
989 1003
990 1004 source_repo = pull_request.source_repo.scm_instance()
991 1005 target_repo = pull_request.target_repo.scm_instance()
992 1006
993 1007 # re-compute commit ids
994 1008 old_commit_ids = pull_request.revisions
995 1009 pre_load = ["author", "date", "message", "branch"]
996 1010 commit_ranges = target_repo.compare(
997 1011 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
998 1012 pre_load=pre_load)
999 1013
1000 1014 target_ref = target_commit.raw_id
1001 1015 source_ref = source_commit.raw_id
1002 1016 ancestor_commit_id = target_repo.get_common_ancestor(
1003 1017 target_ref, source_ref, source_repo)
1004 1018
1005 1019 if not ancestor_commit_id:
1006 1020 raise ValueError(
1007 1021 'cannot calculate diff info without a common ancestor. '
1008 1022 'Make sure both repositories are related, and have a common forking commit.')
1009 1023
1010 1024 pull_request.common_ancestor_id = ancestor_commit_id
1011 1025
1012 1026 pull_request.source_ref = '%s:%s:%s' % (
1013 1027 source_ref_type, source_ref_name, source_commit.raw_id)
1014 1028 pull_request.target_ref = '%s:%s:%s' % (
1015 1029 target_ref_type, target_ref_name, ancestor_commit_id)
1016 1030
1017 1031 pull_request.revisions = [
1018 1032 commit.raw_id for commit in reversed(commit_ranges)]
1019 1033 pull_request.updated_on = datetime.datetime.now()
1020 1034 Session().add(pull_request)
1021 1035 new_commit_ids = pull_request.revisions
1022 1036
1023 1037 old_diff_data, new_diff_data = self._generate_update_diffs(
1024 1038 pull_request, pull_request_version)
1025 1039
1026 1040 # calculate commit and file changes
1027 1041 commit_changes = self._calculate_commit_id_changes(
1028 1042 old_commit_ids, new_commit_ids)
1029 1043 file_changes = self._calculate_file_changes(
1030 1044 old_diff_data, new_diff_data)
1031 1045
1032 1046 # set comments as outdated if DIFFS changed
1033 1047 CommentsModel().outdate_comments(
1034 1048 pull_request, old_diff_data=old_diff_data,
1035 1049 new_diff_data=new_diff_data)
1036 1050
1037 1051 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1038 1052 file_node_changes = (
1039 1053 file_changes.added or file_changes.modified or file_changes.removed)
1040 1054 pr_has_changes = valid_commit_changes or file_node_changes
1041 1055
1042 1056 # Add an automatic comment to the pull request, in case
1043 1057 # anything has changed
1044 1058 if pr_has_changes:
1045 1059 update_comment = CommentsModel().create(
1046 1060 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1047 1061 repo=pull_request.target_repo,
1048 1062 user=pull_request.author,
1049 1063 pull_request=pull_request,
1050 1064 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1051 1065
1052 1066 # Update status to "Under Review" for added commits
1053 1067 for commit_id in commit_changes.added:
1054 1068 ChangesetStatusModel().set_status(
1055 1069 repo=pull_request.source_repo,
1056 1070 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1057 1071 comment=update_comment,
1058 1072 user=pull_request.author,
1059 1073 pull_request=pull_request,
1060 1074 revision=commit_id)
1061 1075
1062 1076 # send update email to users
1063 1077 try:
1064 1078 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1065 1079 ancestor_commit_id=ancestor_commit_id,
1066 1080 commit_changes=commit_changes,
1067 1081 file_changes=file_changes)
1068 1082 except Exception:
1069 1083 log.exception('Failed to send email notification to users')
1070 1084
1071 1085 log.debug(
1072 1086 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1073 1087 'removed_ids: %s', pull_request.pull_request_id,
1074 1088 commit_changes.added, commit_changes.common, commit_changes.removed)
1075 1089 log.debug(
1076 1090 'Updated pull request with the following file changes: %s',
1077 1091 file_changes)
1078 1092
1079 1093 log.info(
1080 1094 "Updated pull request %s from commit %s to commit %s, "
1081 1095 "stored new version %s of this pull request.",
1082 1096 pull_request.pull_request_id, source_ref_id,
1083 1097 pull_request.source_ref_parts.commit_id,
1084 1098 pull_request_version.pull_request_version_id)
1085 1099 Session().commit()
1086 1100 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1087 1101
1088 1102 return UpdateResponse(
1089 1103 executed=True, reason=UpdateFailureReason.NONE,
1090 1104 old=pull_request, new=pull_request_version,
1091 1105 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1092 1106 source_changed=source_changed, target_changed=target_changed)
1093 1107
1094 1108 def _create_version_from_snapshot(self, pull_request):
1095 1109 version = PullRequestVersion()
1096 1110 version.title = pull_request.title
1097 1111 version.description = pull_request.description
1098 1112 version.status = pull_request.status
1099 1113 version.pull_request_state = pull_request.pull_request_state
1100 1114 version.created_on = datetime.datetime.now()
1101 1115 version.updated_on = pull_request.updated_on
1102 1116 version.user_id = pull_request.user_id
1103 1117 version.source_repo = pull_request.source_repo
1104 1118 version.source_ref = pull_request.source_ref
1105 1119 version.target_repo = pull_request.target_repo
1106 1120 version.target_ref = pull_request.target_ref
1107 1121
1108 1122 version._last_merge_source_rev = pull_request._last_merge_source_rev
1109 1123 version._last_merge_target_rev = pull_request._last_merge_target_rev
1110 1124 version.last_merge_status = pull_request.last_merge_status
1111 1125 version.last_merge_metadata = pull_request.last_merge_metadata
1112 1126 version.shadow_merge_ref = pull_request.shadow_merge_ref
1113 1127 version.merge_rev = pull_request.merge_rev
1114 1128 version.reviewer_data = pull_request.reviewer_data
1115 1129
1116 1130 version.revisions = pull_request.revisions
1117 1131 version.common_ancestor_id = pull_request.common_ancestor_id
1118 1132 version.pull_request = pull_request
1119 1133 Session().add(version)
1120 1134 Session().flush()
1121 1135
1122 1136 return version
1123 1137
1124 1138 def _generate_update_diffs(self, pull_request, pull_request_version):
1125 1139
1126 1140 diff_context = (
1127 1141 self.DIFF_CONTEXT +
1128 1142 CommentsModel.needed_extra_diff_context())
1129 1143 hide_whitespace_changes = False
1130 1144 source_repo = pull_request_version.source_repo
1131 1145 source_ref_id = pull_request_version.source_ref_parts.commit_id
1132 1146 target_ref_id = pull_request_version.target_ref_parts.commit_id
1133 1147 old_diff = self._get_diff_from_pr_or_version(
1134 1148 source_repo, source_ref_id, target_ref_id,
1135 1149 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1136 1150
1137 1151 source_repo = pull_request.source_repo
1138 1152 source_ref_id = pull_request.source_ref_parts.commit_id
1139 1153 target_ref_id = pull_request.target_ref_parts.commit_id
1140 1154
1141 1155 new_diff = self._get_diff_from_pr_or_version(
1142 1156 source_repo, source_ref_id, target_ref_id,
1143 1157 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1144 1158
1145 1159 old_diff_data = diffs.DiffProcessor(old_diff)
1146 1160 old_diff_data.prepare()
1147 1161 new_diff_data = diffs.DiffProcessor(new_diff)
1148 1162 new_diff_data.prepare()
1149 1163
1150 1164 return old_diff_data, new_diff_data
1151 1165
1152 1166 def _link_comments_to_version(self, pull_request_version):
1153 1167 """
1154 1168 Link all unlinked comments of this pull request to the given version.
1155 1169
1156 1170 :param pull_request_version: The `PullRequestVersion` to which
1157 1171 the comments shall be linked.
1158 1172
1159 1173 """
1160 1174 pull_request = pull_request_version.pull_request
1161 1175 comments = ChangesetComment.query()\
1162 1176 .filter(
1163 1177 # TODO: johbo: Should we query for the repo at all here?
1164 1178 # Pending decision on how comments of PRs are to be related
1165 1179 # to either the source repo, the target repo or no repo at all.
1166 1180 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1167 1181 ChangesetComment.pull_request == pull_request,
1168 1182 ChangesetComment.pull_request_version == None)\
1169 1183 .order_by(ChangesetComment.comment_id.asc())
1170 1184
1171 1185 # TODO: johbo: Find out why this breaks if it is done in a bulk
1172 1186 # operation.
1173 1187 for comment in comments:
1174 1188 comment.pull_request_version_id = (
1175 1189 pull_request_version.pull_request_version_id)
1176 1190 Session().add(comment)
1177 1191
1178 1192 def _calculate_commit_id_changes(self, old_ids, new_ids):
1179 1193 added = [x for x in new_ids if x not in old_ids]
1180 1194 common = [x for x in new_ids if x in old_ids]
1181 1195 removed = [x for x in old_ids if x not in new_ids]
1182 1196 total = new_ids
1183 1197 return ChangeTuple(added, common, removed, total)
1184 1198
1185 1199 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1186 1200
1187 1201 old_files = OrderedDict()
1188 1202 for diff_data in old_diff_data.parsed_diff:
1189 1203 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1190 1204
1191 1205 added_files = []
1192 1206 modified_files = []
1193 1207 removed_files = []
1194 1208 for diff_data in new_diff_data.parsed_diff:
1195 1209 new_filename = diff_data['filename']
1196 1210 new_hash = md5_safe(diff_data['raw_diff'])
1197 1211
1198 1212 old_hash = old_files.get(new_filename)
1199 1213 if not old_hash:
1200 1214 # file is not present in old diff, we have to figure out from parsed diff
1201 1215 # operation ADD/REMOVE
1202 1216 operations_dict = diff_data['stats']['ops']
1203 1217 if diffs.DEL_FILENODE in operations_dict:
1204 1218 removed_files.append(new_filename)
1205 1219 else:
1206 1220 added_files.append(new_filename)
1207 1221 else:
1208 1222 if new_hash != old_hash:
1209 1223 modified_files.append(new_filename)
1210 1224 # now remove a file from old, since we have seen it already
1211 1225 del old_files[new_filename]
1212 1226
1213 1227 # removed files is when there are present in old, but not in NEW,
1214 1228 # since we remove old files that are present in new diff, left-overs
1215 1229 # if any should be the removed files
1216 1230 removed_files.extend(old_files.keys())
1217 1231
1218 1232 return FileChangeTuple(added_files, modified_files, removed_files)
1219 1233
1220 1234 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1221 1235 """
1222 1236 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1223 1237 so it's always looking the same disregarding on which default
1224 1238 renderer system is using.
1225 1239
1226 1240 :param ancestor_commit_id: ancestor raw_id
1227 1241 :param changes: changes named tuple
1228 1242 :param file_changes: file changes named tuple
1229 1243
1230 1244 """
1231 1245 new_status = ChangesetStatus.get_status_lbl(
1232 1246 ChangesetStatus.STATUS_UNDER_REVIEW)
1233 1247
1234 1248 changed_files = (
1235 1249 file_changes.added + file_changes.modified + file_changes.removed)
1236 1250
1237 1251 params = {
1238 1252 'under_review_label': new_status,
1239 1253 'added_commits': changes.added,
1240 1254 'removed_commits': changes.removed,
1241 1255 'changed_files': changed_files,
1242 1256 'added_files': file_changes.added,
1243 1257 'modified_files': file_changes.modified,
1244 1258 'removed_files': file_changes.removed,
1245 1259 'ancestor_commit_id': ancestor_commit_id
1246 1260 }
1247 1261 renderer = RstTemplateRenderer()
1248 1262 return renderer.render('pull_request_update.mako', **params)
1249 1263
1250 1264 def edit(self, pull_request, title, description, description_renderer, user):
1251 1265 pull_request = self.__get_pull_request(pull_request)
1252 1266 old_data = pull_request.get_api_data(with_merge_state=False)
1253 1267 if pull_request.is_closed():
1254 1268 raise ValueError('This pull request is closed')
1255 1269 if title:
1256 1270 pull_request.title = title
1257 1271 pull_request.description = description
1258 1272 pull_request.updated_on = datetime.datetime.now()
1259 1273 pull_request.description_renderer = description_renderer
1260 1274 Session().add(pull_request)
1261 1275 self._log_audit_action(
1262 1276 'repo.pull_request.edit', {'old_data': old_data},
1263 1277 user, pull_request)
1264 1278
1265 1279 def update_reviewers(self, pull_request, reviewer_data, user):
1266 1280 """
1267 1281 Update the reviewers in the pull request
1268 1282
1269 1283 :param pull_request: the pr to update
1270 1284 :param reviewer_data: list of tuples
1271 1285 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1272 1286 :param user: current use who triggers this action
1273 1287 """
1274 1288
1275 1289 pull_request = self.__get_pull_request(pull_request)
1276 1290 if pull_request.is_closed():
1277 1291 raise ValueError('This pull request is closed')
1278 1292
1279 1293 reviewers = {}
1280 1294 for user_id, reasons, mandatory, role, rules in reviewer_data:
1281 1295 if isinstance(user_id, (int, compat.string_types)):
1282 1296 user_id = self._get_user(user_id).user_id
1283 1297 reviewers[user_id] = {
1284 1298 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1285 1299
1286 1300 reviewers_ids = set(reviewers.keys())
1287 1301 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1288 1302 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1289 1303
1290 1304 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1291 1305
1292 1306 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1293 1307 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1294 1308
1295 1309 log.debug("Adding %s reviewers", ids_to_add)
1296 1310 log.debug("Removing %s reviewers", ids_to_remove)
1297 1311 changed = False
1298 1312 added_audit_reviewers = []
1299 1313 removed_audit_reviewers = []
1300 1314
1301 1315 for uid in ids_to_add:
1302 1316 changed = True
1303 1317 _usr = self._get_user(uid)
1304 1318 reviewer = PullRequestReviewers()
1305 1319 reviewer.user = _usr
1306 1320 reviewer.pull_request = pull_request
1307 1321 reviewer.reasons = reviewers[uid]['reasons']
1308 1322 # NOTE(marcink): mandatory shouldn't be changed now
1309 1323 # reviewer.mandatory = reviewers[uid]['reasons']
1310 1324 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1311 1325 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1312 1326 Session().add(reviewer)
1313 1327 added_audit_reviewers.append(reviewer.get_dict())
1314 1328
1315 1329 for uid in ids_to_remove:
1316 1330 changed = True
1317 1331 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1318 1332 # This is an edge case that handles previous state of having the same reviewer twice.
1319 1333 # this CAN happen due to the lack of DB checks
1320 1334 reviewers = PullRequestReviewers.query()\
1321 1335 .filter(PullRequestReviewers.user_id == uid,
1322 1336 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1323 1337 PullRequestReviewers.pull_request == pull_request)\
1324 1338 .all()
1325 1339
1326 1340 for obj in reviewers:
1327 1341 added_audit_reviewers.append(obj.get_dict())
1328 1342 Session().delete(obj)
1329 1343
1330 1344 if changed:
1331 1345 Session().expire_all()
1332 1346 pull_request.updated_on = datetime.datetime.now()
1333 1347 Session().add(pull_request)
1334 1348
1335 1349 # finally store audit logs
1336 1350 for user_data in added_audit_reviewers:
1337 1351 self._log_audit_action(
1338 1352 'repo.pull_request.reviewer.add', {'data': user_data},
1339 1353 user, pull_request)
1340 1354 for user_data in removed_audit_reviewers:
1341 1355 self._log_audit_action(
1342 1356 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1343 1357 user, pull_request)
1344 1358
1345 1359 self.notify_reviewers(pull_request, ids_to_add, user)
1346 1360 return ids_to_add, ids_to_remove
1347 1361
1348 1362 def update_observers(self, pull_request, observer_data, user):
1349 1363 """
1350 1364 Update the observers in the pull request
1351 1365
1352 1366 :param pull_request: the pr to update
1353 1367 :param observer_data: list of tuples
1354 1368 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1355 1369 :param user: current use who triggers this action
1356 1370 """
1357 1371 pull_request = self.__get_pull_request(pull_request)
1358 1372 if pull_request.is_closed():
1359 1373 raise ValueError('This pull request is closed')
1360 1374
1361 1375 observers = {}
1362 1376 for user_id, reasons, mandatory, role, rules in observer_data:
1363 1377 if isinstance(user_id, (int, compat.string_types)):
1364 1378 user_id = self._get_user(user_id).user_id
1365 1379 observers[user_id] = {
1366 1380 'reasons': reasons, 'observers': mandatory, 'role': role}
1367 1381
1368 1382 observers_ids = set(observers.keys())
1369 1383 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1370 1384 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1371 1385
1372 1386 current_observers_ids = set([x.user.user_id for x in current_observers])
1373 1387
1374 1388 ids_to_add = observers_ids.difference(current_observers_ids)
1375 1389 ids_to_remove = current_observers_ids.difference(observers_ids)
1376 1390
1377 1391 log.debug("Adding %s observer", ids_to_add)
1378 1392 log.debug("Removing %s observer", ids_to_remove)
1379 1393 changed = False
1380 1394 added_audit_observers = []
1381 1395 removed_audit_observers = []
1382 1396
1383 1397 for uid in ids_to_add:
1384 1398 changed = True
1385 1399 _usr = self._get_user(uid)
1386 1400 observer = PullRequestReviewers()
1387 1401 observer.user = _usr
1388 1402 observer.pull_request = pull_request
1389 1403 observer.reasons = observers[uid]['reasons']
1390 1404 # NOTE(marcink): mandatory shouldn't be changed now
1391 1405 # observer.mandatory = observer[uid]['reasons']
1392 1406
1393 1407 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1394 1408 observer.role = PullRequestReviewers.ROLE_OBSERVER
1395 1409 Session().add(observer)
1396 1410 added_audit_observers.append(observer.get_dict())
1397 1411
1398 1412 for uid in ids_to_remove:
1399 1413 changed = True
1400 1414 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1401 1415 # This is an edge case that handles previous state of having the same reviewer twice.
1402 1416 # this CAN happen due to the lack of DB checks
1403 1417 observers = PullRequestReviewers.query()\
1404 1418 .filter(PullRequestReviewers.user_id == uid,
1405 1419 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1406 1420 PullRequestReviewers.pull_request == pull_request)\
1407 1421 .all()
1408 1422
1409 1423 for obj in observers:
1410 1424 added_audit_observers.append(obj.get_dict())
1411 1425 Session().delete(obj)
1412 1426
1413 1427 if changed:
1414 1428 Session().expire_all()
1415 1429 pull_request.updated_on = datetime.datetime.now()
1416 1430 Session().add(pull_request)
1417 1431
1418 1432 # finally store audit logs
1419 1433 for user_data in added_audit_observers:
1420 1434 self._log_audit_action(
1421 1435 'repo.pull_request.observer.add', {'data': user_data},
1422 1436 user, pull_request)
1423 1437 for user_data in removed_audit_observers:
1424 1438 self._log_audit_action(
1425 1439 'repo.pull_request.observer.delete', {'old_data': user_data},
1426 1440 user, pull_request)
1427 1441
1428 1442 self.notify_observers(pull_request, ids_to_add, user)
1429 1443 return ids_to_add, ids_to_remove
1430 1444
1431 1445 def get_url(self, pull_request, request=None, permalink=False):
1432 1446 if not request:
1433 1447 request = get_current_request()
1434 1448
1435 1449 if permalink:
1436 1450 return request.route_url(
1437 1451 'pull_requests_global',
1438 1452 pull_request_id=pull_request.pull_request_id,)
1439 1453 else:
1440 1454 return request.route_url('pullrequest_show',
1441 1455 repo_name=safe_str(pull_request.target_repo.repo_name),
1442 1456 pull_request_id=pull_request.pull_request_id,)
1443 1457
1444 1458 def get_shadow_clone_url(self, pull_request, request=None):
1445 1459 """
1446 1460 Returns qualified url pointing to the shadow repository. If this pull
1447 1461 request is closed there is no shadow repository and ``None`` will be
1448 1462 returned.
1449 1463 """
1450 1464 if pull_request.is_closed():
1451 1465 return None
1452 1466 else:
1453 1467 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1454 1468 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1455 1469
1456 1470 def _notify_reviewers(self, pull_request, user_ids, role, user):
1457 1471 # notification to reviewers/observers
1458 1472 if not user_ids:
1459 1473 return
1460 1474
1461 1475 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1462 1476
1463 1477 pull_request_obj = pull_request
1464 1478 # get the current participants of this pull request
1465 1479 recipients = user_ids
1466 1480 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1467 1481
1468 1482 pr_source_repo = pull_request_obj.source_repo
1469 1483 pr_target_repo = pull_request_obj.target_repo
1470 1484
1471 1485 pr_url = h.route_url('pullrequest_show',
1472 1486 repo_name=pr_target_repo.repo_name,
1473 1487 pull_request_id=pull_request_obj.pull_request_id,)
1474 1488
1475 1489 # set some variables for email notification
1476 1490 pr_target_repo_url = h.route_url(
1477 1491 'repo_summary', repo_name=pr_target_repo.repo_name)
1478 1492
1479 1493 pr_source_repo_url = h.route_url(
1480 1494 'repo_summary', repo_name=pr_source_repo.repo_name)
1481 1495
1482 1496 # pull request specifics
1483 1497 pull_request_commits = [
1484 1498 (x.raw_id, x.message)
1485 1499 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1486 1500
1487 1501 current_rhodecode_user = user
1488 1502 kwargs = {
1489 1503 'user': current_rhodecode_user,
1490 1504 'pull_request_author': pull_request.author,
1491 1505 'pull_request': pull_request_obj,
1492 1506 'pull_request_commits': pull_request_commits,
1493 1507
1494 1508 'pull_request_target_repo': pr_target_repo,
1495 1509 'pull_request_target_repo_url': pr_target_repo_url,
1496 1510
1497 1511 'pull_request_source_repo': pr_source_repo,
1498 1512 'pull_request_source_repo_url': pr_source_repo_url,
1499 1513
1500 1514 'pull_request_url': pr_url,
1501 1515 'thread_ids': [pr_url],
1502 1516 'user_role': role
1503 1517 }
1504 1518
1505 1519 # create notification objects, and emails
1506 1520 NotificationModel().create(
1507 1521 created_by=current_rhodecode_user,
1508 1522 notification_subject='', # Filled in based on the notification_type
1509 1523 notification_body='', # Filled in based on the notification_type
1510 1524 notification_type=notification_type,
1511 1525 recipients=recipients,
1512 1526 email_kwargs=kwargs,
1513 1527 )
1514 1528
1515 1529 def notify_reviewers(self, pull_request, reviewers_ids, user):
1516 1530 return self._notify_reviewers(pull_request, reviewers_ids,
1517 1531 PullRequestReviewers.ROLE_REVIEWER, user)
1518 1532
1519 1533 def notify_observers(self, pull_request, observers_ids, user):
1520 1534 return self._notify_reviewers(pull_request, observers_ids,
1521 1535 PullRequestReviewers.ROLE_OBSERVER, user)
1522 1536
1523 1537 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1524 1538 commit_changes, file_changes):
1525 1539
1526 1540 updating_user_id = updating_user.user_id
1527 1541 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1528 1542 # NOTE(marcink): send notification to all other users except to
1529 1543 # person who updated the PR
1530 1544 recipients = reviewers.difference(set([updating_user_id]))
1531 1545
1532 1546 log.debug('Notify following recipients about pull-request update %s', recipients)
1533 1547
1534 1548 pull_request_obj = pull_request
1535 1549
1536 1550 # send email about the update
1537 1551 changed_files = (
1538 1552 file_changes.added + file_changes.modified + file_changes.removed)
1539 1553
1540 1554 pr_source_repo = pull_request_obj.source_repo
1541 1555 pr_target_repo = pull_request_obj.target_repo
1542 1556
1543 1557 pr_url = h.route_url('pullrequest_show',
1544 1558 repo_name=pr_target_repo.repo_name,
1545 1559 pull_request_id=pull_request_obj.pull_request_id,)
1546 1560
1547 1561 # set some variables for email notification
1548 1562 pr_target_repo_url = h.route_url(
1549 1563 'repo_summary', repo_name=pr_target_repo.repo_name)
1550 1564
1551 1565 pr_source_repo_url = h.route_url(
1552 1566 'repo_summary', repo_name=pr_source_repo.repo_name)
1553 1567
1554 1568 email_kwargs = {
1555 1569 'date': datetime.datetime.now(),
1556 1570 'updating_user': updating_user,
1557 1571
1558 1572 'pull_request': pull_request_obj,
1559 1573
1560 1574 'pull_request_target_repo': pr_target_repo,
1561 1575 'pull_request_target_repo_url': pr_target_repo_url,
1562 1576
1563 1577 'pull_request_source_repo': pr_source_repo,
1564 1578 'pull_request_source_repo_url': pr_source_repo_url,
1565 1579
1566 1580 'pull_request_url': pr_url,
1567 1581
1568 1582 'ancestor_commit_id': ancestor_commit_id,
1569 1583 'added_commits': commit_changes.added,
1570 1584 'removed_commits': commit_changes.removed,
1571 1585 'changed_files': changed_files,
1572 1586 'added_files': file_changes.added,
1573 1587 'modified_files': file_changes.modified,
1574 1588 'removed_files': file_changes.removed,
1575 1589 'thread_ids': [pr_url],
1576 1590 }
1577 1591
1578 1592 # create notification objects, and emails
1579 1593 NotificationModel().create(
1580 1594 created_by=updating_user,
1581 1595 notification_subject='', # Filled in based on the notification_type
1582 1596 notification_body='', # Filled in based on the notification_type
1583 1597 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1584 1598 recipients=recipients,
1585 1599 email_kwargs=email_kwargs,
1586 1600 )
1587 1601
1588 1602 def delete(self, pull_request, user=None):
1589 1603 if not user:
1590 1604 user = getattr(get_current_rhodecode_user(), 'username', None)
1591 1605
1592 1606 pull_request = self.__get_pull_request(pull_request)
1593 1607 old_data = pull_request.get_api_data(with_merge_state=False)
1594 1608 self._cleanup_merge_workspace(pull_request)
1595 1609 self._log_audit_action(
1596 1610 'repo.pull_request.delete', {'old_data': old_data},
1597 1611 user, pull_request)
1598 1612 Session().delete(pull_request)
1599 1613
1600 1614 def close_pull_request(self, pull_request, user):
1601 1615 pull_request = self.__get_pull_request(pull_request)
1602 1616 self._cleanup_merge_workspace(pull_request)
1603 1617 pull_request.status = PullRequest.STATUS_CLOSED
1604 1618 pull_request.updated_on = datetime.datetime.now()
1605 1619 Session().add(pull_request)
1606 1620 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1607 1621
1608 1622 pr_data = pull_request.get_api_data(with_merge_state=False)
1609 1623 self._log_audit_action(
1610 1624 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1611 1625
1612 1626 def close_pull_request_with_comment(
1613 1627 self, pull_request, user, repo, message=None, auth_user=None):
1614 1628
1615 1629 pull_request_review_status = pull_request.calculated_review_status()
1616 1630
1617 1631 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1618 1632 # approved only if we have voting consent
1619 1633 status = ChangesetStatus.STATUS_APPROVED
1620 1634 else:
1621 1635 status = ChangesetStatus.STATUS_REJECTED
1622 1636 status_lbl = ChangesetStatus.get_status_lbl(status)
1623 1637
1624 1638 default_message = (
1625 1639 'Closing with status change {transition_icon} {status}.'
1626 1640 ).format(transition_icon='>', status=status_lbl)
1627 1641 text = message or default_message
1628 1642
1629 1643 # create a comment, and link it to new status
1630 1644 comment = CommentsModel().create(
1631 1645 text=text,
1632 1646 repo=repo.repo_id,
1633 1647 user=user.user_id,
1634 1648 pull_request=pull_request.pull_request_id,
1635 1649 status_change=status_lbl,
1636 1650 status_change_type=status,
1637 1651 closing_pr=True,
1638 1652 auth_user=auth_user,
1639 1653 )
1640 1654
1641 1655 # calculate old status before we change it
1642 1656 old_calculated_status = pull_request.calculated_review_status()
1643 1657 ChangesetStatusModel().set_status(
1644 1658 repo.repo_id,
1645 1659 status,
1646 1660 user.user_id,
1647 1661 comment=comment,
1648 1662 pull_request=pull_request.pull_request_id
1649 1663 )
1650 1664
1651 1665 Session().flush()
1652 1666
1653 1667 self.trigger_pull_request_hook(pull_request, user, 'comment',
1654 1668 data={'comment': comment})
1655 1669
1656 1670 # we now calculate the status of pull request again, and based on that
1657 1671 # calculation trigger status change. This might happen in cases
1658 1672 # that non-reviewer admin closes a pr, which means his vote doesn't
1659 1673 # change the status, while if he's a reviewer this might change it.
1660 1674 calculated_status = pull_request.calculated_review_status()
1661 1675 if old_calculated_status != calculated_status:
1662 1676 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1663 1677 data={'status': calculated_status})
1664 1678
1665 1679 # finally close the PR
1666 1680 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1667 1681
1668 1682 return comment, status
1669 1683
1670 1684 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1671 1685 _ = translator or get_current_request().translate
1672 1686
1673 1687 if not self._is_merge_enabled(pull_request):
1674 1688 return None, False, _('Server-side pull request merging is disabled.')
1675 1689
1676 1690 if pull_request.is_closed():
1677 1691 return None, False, _('This pull request is closed.')
1678 1692
1679 1693 merge_possible, msg = self._check_repo_requirements(
1680 1694 target=pull_request.target_repo, source=pull_request.source_repo,
1681 1695 translator=_)
1682 1696 if not merge_possible:
1683 1697 return None, merge_possible, msg
1684 1698
1685 1699 try:
1686 1700 merge_response = self._try_merge(
1687 1701 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1688 1702 log.debug("Merge response: %s", merge_response)
1689 1703 return merge_response, merge_response.possible, merge_response.merge_status_message
1690 1704 except NotImplementedError:
1691 1705 return None, False, _('Pull request merging is not supported.')
1692 1706
1693 1707 def _check_repo_requirements(self, target, source, translator):
1694 1708 """
1695 1709 Check if `target` and `source` have compatible requirements.
1696 1710
1697 1711 Currently this is just checking for largefiles.
1698 1712 """
1699 1713 _ = translator
1700 1714 target_has_largefiles = self._has_largefiles(target)
1701 1715 source_has_largefiles = self._has_largefiles(source)
1702 1716 merge_possible = True
1703 1717 message = u''
1704 1718
1705 1719 if target_has_largefiles != source_has_largefiles:
1706 1720 merge_possible = False
1707 1721 if source_has_largefiles:
1708 1722 message = _(
1709 1723 'Target repository large files support is disabled.')
1710 1724 else:
1711 1725 message = _(
1712 1726 'Source repository large files support is disabled.')
1713 1727
1714 1728 return merge_possible, message
1715 1729
1716 1730 def _has_largefiles(self, repo):
1717 1731 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1718 1732 'extensions', 'largefiles')
1719 1733 return largefiles_ui and largefiles_ui[0].active
1720 1734
1721 1735 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1722 1736 """
1723 1737 Try to merge the pull request and return the merge status.
1724 1738 """
1725 1739 log.debug(
1726 1740 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1727 1741 pull_request.pull_request_id, force_shadow_repo_refresh)
1728 1742 target_vcs = pull_request.target_repo.scm_instance()
1729 1743 # Refresh the target reference.
1730 1744 try:
1731 1745 target_ref = self._refresh_reference(
1732 1746 pull_request.target_ref_parts, target_vcs)
1733 1747 except CommitDoesNotExistError:
1734 1748 merge_state = MergeResponse(
1735 1749 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1736 1750 metadata={'target_ref': pull_request.target_ref_parts})
1737 1751 return merge_state
1738 1752
1739 1753 target_locked = pull_request.target_repo.locked
1740 1754 if target_locked and target_locked[0]:
1741 1755 locked_by = 'user:{}'.format(target_locked[0])
1742 1756 log.debug("The target repository is locked by %s.", locked_by)
1743 1757 merge_state = MergeResponse(
1744 1758 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1745 1759 metadata={'locked_by': locked_by})
1746 1760 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1747 1761 pull_request, target_ref):
1748 1762 log.debug("Refreshing the merge status of the repository.")
1749 1763 merge_state = self._refresh_merge_state(
1750 1764 pull_request, target_vcs, target_ref)
1751 1765 else:
1752 1766 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1753 1767 metadata = {
1754 1768 'unresolved_files': '',
1755 1769 'target_ref': pull_request.target_ref_parts,
1756 1770 'source_ref': pull_request.source_ref_parts,
1757 1771 }
1758 1772 if pull_request.last_merge_metadata:
1759 1773 metadata.update(pull_request.last_merge_metadata_parsed)
1760 1774
1761 1775 if not possible and target_ref.type == 'branch':
1762 1776 # NOTE(marcink): case for mercurial multiple heads on branch
1763 1777 heads = target_vcs._heads(target_ref.name)
1764 1778 if len(heads) != 1:
1765 1779 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1766 1780 metadata.update({
1767 1781 'heads': heads
1768 1782 })
1769 1783
1770 1784 merge_state = MergeResponse(
1771 1785 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1772 1786
1773 1787 return merge_state
1774 1788
1775 1789 def _refresh_reference(self, reference, vcs_repository):
1776 1790 if reference.type in self.UPDATABLE_REF_TYPES:
1777 1791 name_or_id = reference.name
1778 1792 else:
1779 1793 name_or_id = reference.commit_id
1780 1794
1781 1795 refreshed_commit = vcs_repository.get_commit(name_or_id)
1782 1796 refreshed_reference = Reference(
1783 1797 reference.type, reference.name, refreshed_commit.raw_id)
1784 1798 return refreshed_reference
1785 1799
1786 1800 def _needs_merge_state_refresh(self, pull_request, target_reference):
1787 1801 return not(
1788 1802 pull_request.revisions and
1789 1803 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1790 1804 target_reference.commit_id == pull_request._last_merge_target_rev)
1791 1805
1792 1806 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1793 1807 workspace_id = self._workspace_id(pull_request)
1794 1808 source_vcs = pull_request.source_repo.scm_instance()
1795 1809 repo_id = pull_request.target_repo.repo_id
1796 1810 use_rebase = self._use_rebase_for_merging(pull_request)
1797 1811 close_branch = self._close_branch_before_merging(pull_request)
1798 1812 merge_state = target_vcs.merge(
1799 1813 repo_id, workspace_id,
1800 1814 target_reference, source_vcs, pull_request.source_ref_parts,
1801 1815 dry_run=True, use_rebase=use_rebase,
1802 1816 close_branch=close_branch)
1803 1817
1804 1818 # Do not store the response if there was an unknown error.
1805 1819 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1806 1820 pull_request._last_merge_source_rev = \
1807 1821 pull_request.source_ref_parts.commit_id
1808 1822 pull_request._last_merge_target_rev = target_reference.commit_id
1809 1823 pull_request.last_merge_status = merge_state.failure_reason
1810 1824 pull_request.last_merge_metadata = merge_state.metadata
1811 1825
1812 1826 pull_request.shadow_merge_ref = merge_state.merge_ref
1813 1827 Session().add(pull_request)
1814 1828 Session().commit()
1815 1829
1816 1830 return merge_state
1817 1831
1818 1832 def _workspace_id(self, pull_request):
1819 1833 workspace_id = 'pr-%s' % pull_request.pull_request_id
1820 1834 return workspace_id
1821 1835
1822 1836 def generate_repo_data(self, repo, commit_id=None, branch=None,
1823 1837 bookmark=None, translator=None):
1824 1838 from rhodecode.model.repo import RepoModel
1825 1839
1826 1840 all_refs, selected_ref = \
1827 1841 self._get_repo_pullrequest_sources(
1828 1842 repo.scm_instance(), commit_id=commit_id,
1829 1843 branch=branch, bookmark=bookmark, translator=translator)
1830 1844
1831 1845 refs_select2 = []
1832 1846 for element in all_refs:
1833 1847 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1834 1848 refs_select2.append({'text': element[1], 'children': children})
1835 1849
1836 1850 return {
1837 1851 'user': {
1838 1852 'user_id': repo.user.user_id,
1839 1853 'username': repo.user.username,
1840 1854 'firstname': repo.user.first_name,
1841 1855 'lastname': repo.user.last_name,
1842 1856 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1843 1857 },
1844 1858 'name': repo.repo_name,
1845 1859 'link': RepoModel().get_url(repo),
1846 1860 'description': h.chop_at_smart(repo.description_safe, '\n'),
1847 1861 'refs': {
1848 1862 'all_refs': all_refs,
1849 1863 'selected_ref': selected_ref,
1850 1864 'select2_refs': refs_select2
1851 1865 }
1852 1866 }
1853 1867
1854 1868 def generate_pullrequest_title(self, source, source_ref, target):
1855 1869 return u'{source}#{at_ref} to {target}'.format(
1856 1870 source=source,
1857 1871 at_ref=source_ref,
1858 1872 target=target,
1859 1873 )
1860 1874
1861 1875 def _cleanup_merge_workspace(self, pull_request):
1862 1876 # Merging related cleanup
1863 1877 repo_id = pull_request.target_repo.repo_id
1864 1878 target_scm = pull_request.target_repo.scm_instance()
1865 1879 workspace_id = self._workspace_id(pull_request)
1866 1880
1867 1881 try:
1868 1882 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1869 1883 except NotImplementedError:
1870 1884 pass
1871 1885
1872 1886 def _get_repo_pullrequest_sources(
1873 1887 self, repo, commit_id=None, branch=None, bookmark=None,
1874 1888 translator=None):
1875 1889 """
1876 1890 Return a structure with repo's interesting commits, suitable for
1877 1891 the selectors in pullrequest controller
1878 1892
1879 1893 :param commit_id: a commit that must be in the list somehow
1880 1894 and selected by default
1881 1895 :param branch: a branch that must be in the list and selected
1882 1896 by default - even if closed
1883 1897 :param bookmark: a bookmark that must be in the list and selected
1884 1898 """
1885 1899 _ = translator or get_current_request().translate
1886 1900
1887 1901 commit_id = safe_str(commit_id) if commit_id else None
1888 1902 branch = safe_unicode(branch) if branch else None
1889 1903 bookmark = safe_unicode(bookmark) if bookmark else None
1890 1904
1891 1905 selected = None
1892 1906
1893 1907 # order matters: first source that has commit_id in it will be selected
1894 1908 sources = []
1895 1909 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1896 1910 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1897 1911
1898 1912 if commit_id:
1899 1913 ref_commit = (h.short_id(commit_id), commit_id)
1900 1914 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1901 1915
1902 1916 sources.append(
1903 1917 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1904 1918 )
1905 1919
1906 1920 groups = []
1907 1921
1908 1922 for group_key, ref_list, group_name, match in sources:
1909 1923 group_refs = []
1910 1924 for ref_name, ref_id in ref_list:
1911 1925 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1912 1926 group_refs.append((ref_key, ref_name))
1913 1927
1914 1928 if not selected:
1915 1929 if set([commit_id, match]) & set([ref_id, ref_name]):
1916 1930 selected = ref_key
1917 1931
1918 1932 if group_refs:
1919 1933 groups.append((group_refs, group_name))
1920 1934
1921 1935 if not selected:
1922 1936 ref = commit_id or branch or bookmark
1923 1937 if ref:
1924 1938 raise CommitDoesNotExistError(
1925 1939 u'No commit refs could be found matching: {}'.format(ref))
1926 1940 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1927 1941 selected = u'branch:{}:{}'.format(
1928 1942 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1929 1943 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1930 1944 )
1931 1945 elif repo.commit_ids:
1932 1946 # make the user select in this case
1933 1947 selected = None
1934 1948 else:
1935 1949 raise EmptyRepositoryError()
1936 1950 return groups, selected
1937 1951
1938 1952 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1939 1953 hide_whitespace_changes, diff_context):
1940 1954
1941 1955 return self._get_diff_from_pr_or_version(
1942 1956 source_repo, source_ref_id, target_ref_id,
1943 1957 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1944 1958
1945 1959 def _get_diff_from_pr_or_version(
1946 1960 self, source_repo, source_ref_id, target_ref_id,
1947 1961 hide_whitespace_changes, diff_context):
1948 1962
1949 1963 target_commit = source_repo.get_commit(
1950 1964 commit_id=safe_str(target_ref_id))
1951 1965 source_commit = source_repo.get_commit(
1952 1966 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1953 1967 if isinstance(source_repo, Repository):
1954 1968 vcs_repo = source_repo.scm_instance()
1955 1969 else:
1956 1970 vcs_repo = source_repo
1957 1971
1958 1972 # TODO: johbo: In the context of an update, we cannot reach
1959 1973 # the old commit anymore with our normal mechanisms. It needs
1960 1974 # some sort of special support in the vcs layer to avoid this
1961 1975 # workaround.
1962 1976 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1963 1977 vcs_repo.alias == 'git'):
1964 1978 source_commit.raw_id = safe_str(source_ref_id)
1965 1979
1966 1980 log.debug('calculating diff between '
1967 1981 'source_ref:%s and target_ref:%s for repo `%s`',
1968 1982 target_ref_id, source_ref_id,
1969 1983 safe_unicode(vcs_repo.path))
1970 1984
1971 1985 vcs_diff = vcs_repo.get_diff(
1972 1986 commit1=target_commit, commit2=source_commit,
1973 1987 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1974 1988 return vcs_diff
1975 1989
1976 1990 def _is_merge_enabled(self, pull_request):
1977 1991 return self._get_general_setting(
1978 1992 pull_request, 'rhodecode_pr_merge_enabled')
1979 1993
1980 1994 def _use_rebase_for_merging(self, pull_request):
1981 1995 repo_type = pull_request.target_repo.repo_type
1982 1996 if repo_type == 'hg':
1983 1997 return self._get_general_setting(
1984 1998 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1985 1999 elif repo_type == 'git':
1986 2000 return self._get_general_setting(
1987 2001 pull_request, 'rhodecode_git_use_rebase_for_merging')
1988 2002
1989 2003 return False
1990 2004
1991 2005 def _user_name_for_merging(self, pull_request, user):
1992 2006 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1993 2007 if env_user_name_attr and hasattr(user, env_user_name_attr):
1994 2008 user_name_attr = env_user_name_attr
1995 2009 else:
1996 2010 user_name_attr = 'short_contact'
1997 2011
1998 2012 user_name = getattr(user, user_name_attr)
1999 2013 return user_name
2000 2014
2001 2015 def _close_branch_before_merging(self, pull_request):
2002 2016 repo_type = pull_request.target_repo.repo_type
2003 2017 if repo_type == 'hg':
2004 2018 return self._get_general_setting(
2005 2019 pull_request, 'rhodecode_hg_close_branch_before_merging')
2006 2020 elif repo_type == 'git':
2007 2021 return self._get_general_setting(
2008 2022 pull_request, 'rhodecode_git_close_branch_before_merging')
2009 2023
2010 2024 return False
2011 2025
2012 2026 def _get_general_setting(self, pull_request, settings_key, default=False):
2013 2027 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2014 2028 settings = settings_model.get_general_settings()
2015 2029 return settings.get(settings_key, default)
2016 2030
2017 2031 def _log_audit_action(self, action, action_data, user, pull_request):
2018 2032 audit_logger.store(
2019 2033 action=action,
2020 2034 action_data=action_data,
2021 2035 user=user,
2022 2036 repo=pull_request.target_repo)
2023 2037
2024 2038 def get_reviewer_functions(self):
2025 2039 """
2026 2040 Fetches functions for validation and fetching default reviewers.
2027 2041 If available we use the EE package, else we fallback to CE
2028 2042 package functions
2029 2043 """
2030 2044 try:
2031 2045 from rc_reviewers.utils import get_default_reviewers_data
2032 2046 from rc_reviewers.utils import validate_default_reviewers
2033 2047 from rc_reviewers.utils import validate_observers
2034 2048 except ImportError:
2035 2049 from rhodecode.apps.repository.utils import get_default_reviewers_data
2036 2050 from rhodecode.apps.repository.utils import validate_default_reviewers
2037 2051 from rhodecode.apps.repository.utils import validate_observers
2038 2052
2039 2053 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2040 2054
2041 2055
2042 2056 class MergeCheck(object):
2043 2057 """
2044 2058 Perform Merge Checks and returns a check object which stores information
2045 2059 about merge errors, and merge conditions
2046 2060 """
2047 2061 TODO_CHECK = 'todo'
2048 2062 PERM_CHECK = 'perm'
2049 2063 REVIEW_CHECK = 'review'
2050 2064 MERGE_CHECK = 'merge'
2051 2065 WIP_CHECK = 'wip'
2052 2066
2053 2067 def __init__(self):
2054 2068 self.review_status = None
2055 2069 self.merge_possible = None
2056 2070 self.merge_msg = ''
2057 2071 self.merge_response = None
2058 2072 self.failed = None
2059 2073 self.errors = []
2060 2074 self.error_details = OrderedDict()
2061 2075 self.source_commit = AttributeDict()
2062 2076 self.target_commit = AttributeDict()
2063 2077 self.reviewers_count = 0
2064 2078 self.observers_count = 0
2065 2079
2066 2080 def __repr__(self):
2067 2081 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2068 2082 self.merge_possible, self.failed, self.errors)
2069 2083
2070 2084 def push_error(self, error_type, message, error_key, details):
2071 2085 self.failed = True
2072 2086 self.errors.append([error_type, message])
2073 2087 self.error_details[error_key] = dict(
2074 2088 details=details,
2075 2089 error_type=error_type,
2076 2090 message=message
2077 2091 )
2078 2092
2079 2093 @classmethod
2080 2094 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2081 2095 force_shadow_repo_refresh=False):
2082 2096 _ = translator
2083 2097 merge_check = cls()
2084 2098
2085 2099 # title has WIP:
2086 2100 if pull_request.work_in_progress:
2087 2101 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2088 2102
2089 2103 msg = _('WIP marker in title prevents from accidental merge.')
2090 2104 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2091 2105 if fail_early:
2092 2106 return merge_check
2093 2107
2094 2108 # permissions to merge
2095 2109 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2096 2110 if not user_allowed_to_merge:
2097 2111 log.debug("MergeCheck: cannot merge, approval is pending.")
2098 2112
2099 2113 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2100 2114 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2101 2115 if fail_early:
2102 2116 return merge_check
2103 2117
2104 2118 # permission to merge into the target branch
2105 2119 target_commit_id = pull_request.target_ref_parts.commit_id
2106 2120 if pull_request.target_ref_parts.type == 'branch':
2107 2121 branch_name = pull_request.target_ref_parts.name
2108 2122 else:
2109 2123 # for mercurial we can always figure out the branch from the commit
2110 2124 # in case of bookmark
2111 2125 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2112 2126 branch_name = target_commit.branch
2113 2127
2114 2128 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2115 2129 pull_request.target_repo.repo_name, branch_name)
2116 2130 if branch_perm and branch_perm == 'branch.none':
2117 2131 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2118 2132 branch_name, rule)
2119 2133 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2120 2134 if fail_early:
2121 2135 return merge_check
2122 2136
2123 2137 # review status, must be always present
2124 2138 review_status = pull_request.calculated_review_status()
2125 2139 merge_check.review_status = review_status
2126 2140 merge_check.reviewers_count = pull_request.reviewers_count
2127 2141 merge_check.observers_count = pull_request.observers_count
2128 2142
2129 2143 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2130 2144 if not status_approved and merge_check.reviewers_count:
2131 2145 log.debug("MergeCheck: cannot merge, approval is pending.")
2132 2146 msg = _('Pull request reviewer approval is pending.')
2133 2147
2134 2148 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2135 2149
2136 2150 if fail_early:
2137 2151 return merge_check
2138 2152
2139 2153 # left over TODOs
2140 2154 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2141 2155 if todos:
2142 2156 log.debug("MergeCheck: cannot merge, {} "
2143 2157 "unresolved TODOs left.".format(len(todos)))
2144 2158
2145 2159 if len(todos) == 1:
2146 2160 msg = _('Cannot merge, {} TODO still not resolved.').format(
2147 2161 len(todos))
2148 2162 else:
2149 2163 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2150 2164 len(todos))
2151 2165
2152 2166 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2153 2167
2154 2168 if fail_early:
2155 2169 return merge_check
2156 2170
2157 2171 # merge possible, here is the filesystem simulation + shadow repo
2158 2172 merge_response, merge_status, msg = PullRequestModel().merge_status(
2159 2173 pull_request, translator=translator,
2160 2174 force_shadow_repo_refresh=force_shadow_repo_refresh)
2161 2175
2162 2176 merge_check.merge_possible = merge_status
2163 2177 merge_check.merge_msg = msg
2164 2178 merge_check.merge_response = merge_response
2165 2179
2166 2180 source_ref_id = pull_request.source_ref_parts.commit_id
2167 2181 target_ref_id = pull_request.target_ref_parts.commit_id
2168 2182
2169 2183 try:
2170 2184 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2171 2185 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2172 2186 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2173 2187 merge_check.source_commit.current_raw_id = source_commit.raw_id
2174 2188 merge_check.source_commit.previous_raw_id = source_ref_id
2175 2189
2176 2190 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2177 2191 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2178 2192 merge_check.target_commit.current_raw_id = target_commit.raw_id
2179 2193 merge_check.target_commit.previous_raw_id = target_ref_id
2180 2194 except (SourceRefMissing, TargetRefMissing):
2181 2195 pass
2182 2196
2183 2197 if not merge_status:
2184 2198 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2185 2199 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2186 2200
2187 2201 if fail_early:
2188 2202 return merge_check
2189 2203
2190 2204 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2191 2205 return merge_check
2192 2206
2193 2207 @classmethod
2194 2208 def get_merge_conditions(cls, pull_request, translator):
2195 2209 _ = translator
2196 2210 merge_details = {}
2197 2211
2198 2212 model = PullRequestModel()
2199 2213 use_rebase = model._use_rebase_for_merging(pull_request)
2200 2214
2201 2215 if use_rebase:
2202 2216 merge_details['merge_strategy'] = dict(
2203 2217 details={},
2204 2218 message=_('Merge strategy: rebase')
2205 2219 )
2206 2220 else:
2207 2221 merge_details['merge_strategy'] = dict(
2208 2222 details={},
2209 2223 message=_('Merge strategy: explicit merge commit')
2210 2224 )
2211 2225
2212 2226 close_branch = model._close_branch_before_merging(pull_request)
2213 2227 if close_branch:
2214 2228 repo_type = pull_request.target_repo.repo_type
2215 2229 close_msg = ''
2216 2230 if repo_type == 'hg':
2217 2231 close_msg = _('Source branch will be closed before the merge.')
2218 2232 elif repo_type == 'git':
2219 2233 close_msg = _('Source branch will be deleted after the merge.')
2220 2234
2221 2235 merge_details['close_branch'] = dict(
2222 2236 details={},
2223 2237 message=close_msg
2224 2238 )
2225 2239
2226 2240 return merge_details
2227 2241
2228 2242
2229 2243 ChangeTuple = collections.namedtuple(
2230 2244 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2231 2245
2232 2246 FileChangeTuple = collections.namedtuple(
2233 2247 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,80 +1,88 b''
1 1 ## Changesets table !
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 %if c.ancestor:
5 5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
6 6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
7 7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
8 8 </div>
9 9 %endif
10 10
11 11 <div class="container">
12 12 <input type="hidden" name="__start__" value="revisions:sequence">
13 13 <table class="rctable compare_view_commits">
14 14 <tr>
15 % if hasattr(c, 'commit_versions'):
16 <th>ver</th>
17 % endif
15 18 <th>${_('Time')}</th>
16 19 <th>${_('Author')}</th>
17 20 <th>${_('Commit')}</th>
18 21 <th></th>
19 22 <th>${_('Description')}</th>
20 23 </tr>
21 24 ## to speed up lookups cache some functions before the loop
22 25 <%
23 26 active_patterns = h.get_active_pattern_entries(c.repo_name)
24 27 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
25 28 %>
26 29
27 30 %for commit in c.commit_ranges:
28 31 <tr id="row-${commit.raw_id}"
29 32 commit_id="${commit.raw_id}"
30 33 class="compare_select"
31 34 style="${'display: none' if c.collapse_all_commits else ''}"
32 35 >
36 % if hasattr(c, 'commit_versions'):
37 <td class="tooltip" title="${_('Pull request version this commit was introduced')}">
38 <code>${('v{}'.format(c.commit_versions[commit.raw_id][0]) if c.commit_versions[commit.raw_id] else 'latest')}</code>
39 </td>
40 % endif
33 41 <td class="td-time">
34 42 ${h.age_component(commit.date)}
35 43 </td>
36 44 <td class="td-user">
37 45 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
38 46 </td>
39 47 <td class="td-hash">
40 48 <code>
41 49 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
42 50 r${commit.idx}:${h.short_id(commit.raw_id)}
43 51 </a>
44 52 ${h.hidden('revisions',commit.raw_id)}
45 53 </code>
46 54 </td>
47 55 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
48 56 <i class="icon-expand-linked"></i>
49 57 </td>
50 58 <td class="mid td-description">
51 59 <div class="log-container truncate-wrap">
52 60 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
53 61 </div>
54 62 </td>
55 63 </tr>
56 64 %endfor
57 65 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
58 66 <td colspan="5">
59 67 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
60 68 </td>
61 69 </tr>
62 70 % if not c.commit_ranges:
63 71 <tr class="compare_select">
64 72 <td colspan="5">
65 73 ${_('No commits in this compare')}
66 74 </td>
67 75 </tr>
68 76 % endif
69 77 </table>
70 78 <input type="hidden" name="__end__" value="revisions:sequence">
71 79
72 80 </div>
73 81
74 82 <script>
75 83 commitsController = new CommitsController();
76 84 $('.compare_select').on('click',function(e){
77 85 var cid = $(this).attr('commit_id');
78 86 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
79 87 });
80 88 </script>
General Comments 0
You need to be logged in to leave comments. Login now