##// END OF EJS Templates
pull-requests: in case of an error redirect to the same url as source....
marcink -
r2052:7fd6e6f2 default
parent child Browse files
Show More
@@ -1,1187 +1,1191 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import peppercorn
26 26 from pyramid.httpexceptions import (
27 27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
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.ext_json import json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 46 ChangesetComment, ChangesetStatus, Repository)
47 47 from rhodecode.model.forms import PullRequestForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 50 from rhodecode.model.scm import ScmModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 56
57 57 def load_default_context(self):
58 58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 60 c.repo_info = self.db_repo
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 pull_request_id = self.request.matchdict['pull_request_id']
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user)
367 367 c.pr_merge_errors = _merge_check.error_details
368 368 c.pr_merge_possible = not _merge_check.failed
369 369 c.pr_merge_message = _merge_check.merge_msg
370 370
371 371 c.pull_request_review_status = _merge_check.review_status
372 372 if merge_checks:
373 373 self.request.override_renderer = \
374 374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 375 return self._get_template_context(c)
376 376
377 377 comments_model = CommentsModel()
378 378
379 379 # reviewers and statuses
380 380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382 382
383 383 # GENERAL COMMENTS with versions #
384 384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 385 q = q.order_by(ChangesetComment.comment_id.asc())
386 386 general_comments = q
387 387
388 388 # pick comments we want to render at current version
389 389 c.comment_versions = comments_model.aggregate_comments(
390 390 general_comments, versions, c.at_version_num)
391 391 c.comments = c.comment_versions[c.at_version_num]['until']
392 392
393 393 # INLINE COMMENTS with versions #
394 394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 395 q = q.order_by(ChangesetComment.comment_id.asc())
396 396 inline_comments = q
397 397
398 398 c.inline_versions = comments_model.aggregate_comments(
399 399 inline_comments, versions, c.at_version_num, inline=True)
400 400
401 401 # inject latest version
402 402 latest_ver = PullRequest.get_pr_display_object(
403 403 pull_request_latest, pull_request_latest)
404 404
405 405 c.versions = versions + [latest_ver]
406 406
407 407 # if we use version, then do not show later comments
408 408 # than current version
409 409 display_inline_comments = collections.defaultdict(
410 410 lambda: collections.defaultdict(list))
411 411 for co in inline_comments:
412 412 if c.at_version_num:
413 413 # pick comments that are at least UPTO given version, so we
414 414 # don't render comments for higher version
415 415 should_render = co.pull_request_version_id and \
416 416 co.pull_request_version_id <= c.at_version_num
417 417 else:
418 418 # showing all, for 'latest'
419 419 should_render = True
420 420
421 421 if should_render:
422 422 display_inline_comments[co.f_path][co.line_no].append(co)
423 423
424 424 # load diff data into template context, if we use compare mode then
425 425 # diff is calculated based on changes between versions of PR
426 426
427 427 source_repo = pull_request_at_ver.source_repo
428 428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429 429
430 430 target_repo = pull_request_at_ver.target_repo
431 431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432 432
433 433 if compare:
434 434 # in compare switch the diff base to latest commit from prev version
435 435 target_ref_id = prev_pull_request_display_obj.revisions[0]
436 436
437 437 # despite opening commits for bookmarks/branches/tags, we always
438 438 # convert this to rev to prevent changes after bookmark or branch change
439 439 c.source_ref_type = 'rev'
440 440 c.source_ref = source_ref_id
441 441
442 442 c.target_ref_type = 'rev'
443 443 c.target_ref = target_ref_id
444 444
445 445 c.source_repo = source_repo
446 446 c.target_repo = target_repo
447 447
448 448 c.commit_ranges = []
449 449 source_commit = EmptyCommit()
450 450 target_commit = EmptyCommit()
451 451 c.missing_requirements = False
452 452
453 453 source_scm = source_repo.scm_instance()
454 454 target_scm = target_repo.scm_instance()
455 455
456 456 # try first shadow repo, fallback to regular repo
457 457 try:
458 458 commits_source_repo = pull_request_latest.get_shadow_repo()
459 459 except Exception:
460 460 log.debug('Failed to get shadow repo', exc_info=True)
461 461 commits_source_repo = source_scm
462 462
463 463 c.commits_source_repo = commits_source_repo
464 464 commit_cache = {}
465 465 try:
466 466 pre_load = ["author", "branch", "date", "message"]
467 467 show_revs = pull_request_at_ver.revisions
468 468 for rev in show_revs:
469 469 comm = commits_source_repo.get_commit(
470 470 commit_id=rev, pre_load=pre_load)
471 471 c.commit_ranges.append(comm)
472 472 commit_cache[comm.raw_id] = comm
473 473
474 474 # Order here matters, we first need to get target, and then
475 475 # the source
476 476 target_commit = commits_source_repo.get_commit(
477 477 commit_id=safe_str(target_ref_id))
478 478
479 479 source_commit = commits_source_repo.get_commit(
480 480 commit_id=safe_str(source_ref_id))
481 481
482 482 except CommitDoesNotExistError:
483 483 log.warning(
484 484 'Failed to get commit from `{}` repo'.format(
485 485 commits_source_repo), exc_info=True)
486 486 except RepositoryRequirementError:
487 487 log.warning(
488 488 'Failed to get all required data from repo', exc_info=True)
489 489 c.missing_requirements = True
490 490
491 491 c.ancestor = None # set it to None, to hide it from PR view
492 492
493 493 try:
494 494 ancestor_id = source_scm.get_common_ancestor(
495 495 source_commit.raw_id, target_commit.raw_id, target_scm)
496 496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 497 except Exception:
498 498 c.ancestor_commit = None
499 499
500 500 c.statuses = source_repo.statuses(
501 501 [x.raw_id for x in c.commit_ranges])
502 502
503 503 # auto collapse if we have more than limit
504 504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 506 c.compare_mode = compare
507 507
508 508 # diff_limit is the old behavior, will cut off the whole diff
509 509 # if the limit is applied otherwise will just hide the
510 510 # big files from the front-end
511 511 diff_limit = c.visual.cut_off_limit_diff
512 512 file_limit = c.visual.cut_off_limit_file
513 513
514 514 c.missing_commits = False
515 515 if (c.missing_requirements
516 516 or isinstance(source_commit, EmptyCommit)
517 517 or source_commit == target_commit):
518 518
519 519 c.missing_commits = True
520 520 else:
521 521
522 522 c.diffset = self._get_diffset(
523 523 c.source_repo.repo_name, commits_source_repo,
524 524 source_ref_id, target_ref_id,
525 525 target_commit, source_commit,
526 526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527 527
528 528 c.limited_diff = c.diffset.limited_diff
529 529
530 530 # calculate removed files that are bound to comments
531 531 comment_deleted_files = [
532 532 fname for fname in display_inline_comments
533 533 if fname not in c.diffset.file_stats]
534 534
535 535 c.deleted_files_comments = collections.defaultdict(dict)
536 536 for fname, per_line_comments in display_inline_comments.items():
537 537 if fname in comment_deleted_files:
538 538 c.deleted_files_comments[fname]['stats'] = 0
539 539 c.deleted_files_comments[fname]['comments'] = list()
540 540 for lno, comments in per_line_comments.items():
541 541 c.deleted_files_comments[fname]['comments'].extend(
542 542 comments)
543 543
544 544 # this is a hack to properly display links, when creating PR, the
545 545 # compare view and others uses different notation, and
546 546 # compare_commits.mako renders links based on the target_repo.
547 547 # We need to swap that here to generate it properly on the html side
548 548 c.target_repo = c.source_repo
549 549
550 550 c.commit_statuses = ChangesetStatus.STATUSES
551 551
552 552 c.show_version_changes = not pr_closed
553 553 if c.show_version_changes:
554 554 cur_obj = pull_request_at_ver
555 555 prev_obj = prev_pull_request_at_ver
556 556
557 557 old_commit_ids = prev_obj.revisions
558 558 new_commit_ids = cur_obj.revisions
559 559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 560 old_commit_ids, new_commit_ids)
561 561 c.commit_changes_summary = commit_changes
562 562
563 563 # calculate the diff for commits between versions
564 564 c.commit_changes = []
565 565 mark = lambda cs, fw: list(
566 566 h.itertools.izip_longest([], cs, fillvalue=fw))
567 567 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 568 + mark(commit_changes.removed, 'r') \
569 569 + mark(commit_changes.common, 'c'):
570 570
571 571 if raw_id in commit_cache:
572 572 commit = commit_cache[raw_id]
573 573 else:
574 574 try:
575 575 commit = commits_source_repo.get_commit(raw_id)
576 576 except CommitDoesNotExistError:
577 577 # in case we fail extracting still use "dummy" commit
578 578 # for display in commit diff
579 579 commit = h.AttributeDict(
580 580 {'raw_id': raw_id,
581 581 'message': 'EMPTY or MISSING COMMIT'})
582 582 c.commit_changes.append([c_type, commit])
583 583
584 584 # current user review statuses for each version
585 585 c.review_versions = {}
586 586 if self._rhodecode_user.user_id in allowed_reviewers:
587 587 for co in general_comments:
588 588 if co.author.user_id == self._rhodecode_user.user_id:
589 589 # each comment has a status change
590 590 status = co.status_change
591 591 if status:
592 592 _ver_pr = status[0].comment.pull_request_version_id
593 593 c.review_versions[_ver_pr] = status[0]
594 594
595 595 return self._get_template_context(c)
596 596
597 597 def assure_not_empty_repo(self):
598 598 _ = self.request.translate
599 599
600 600 try:
601 601 self.db_repo.scm_instance().get_commit()
602 602 except EmptyRepositoryError:
603 603 h.flash(h.literal(_('There are no commits yet')),
604 604 category='warning')
605 605 raise HTTPFound(
606 606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607 607
608 608 @LoginRequired()
609 609 @NotAnonymous()
610 610 @HasRepoPermissionAnyDecorator(
611 611 'repository.read', 'repository.write', 'repository.admin')
612 612 @view_config(
613 613 route_name='pullrequest_new', request_method='GET',
614 614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 615 def pull_request_new(self):
616 616 _ = self.request.translate
617 617 c = self.load_default_context()
618 618
619 619 self.assure_not_empty_repo()
620 620 source_repo = self.db_repo
621 621
622 622 commit_id = self.request.GET.get('commit')
623 623 branch_ref = self.request.GET.get('branch')
624 624 bookmark_ref = self.request.GET.get('bookmark')
625 625
626 626 try:
627 627 source_repo_data = PullRequestModel().generate_repo_data(
628 628 source_repo, commit_id=commit_id,
629 629 branch=branch_ref, bookmark=bookmark_ref)
630 630 except CommitDoesNotExistError as e:
631 631 log.exception(e)
632 632 h.flash(_('Commit does not exist'), 'error')
633 633 raise HTTPFound(
634 634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635 635
636 636 default_target_repo = source_repo
637 637
638 638 if source_repo.parent:
639 639 parent_vcs_obj = source_repo.parent.scm_instance()
640 640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 641 # change default if we have a parent repo
642 642 default_target_repo = source_repo.parent
643 643
644 644 target_repo_data = PullRequestModel().generate_repo_data(
645 645 default_target_repo)
646 646
647 647 selected_source_ref = source_repo_data['refs']['selected_ref']
648 648
649 649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 651 source=source_repo.repo_name,
652 652 source_ref=title_source_ref,
653 653 target=default_target_repo.repo_name
654 654 )
655 655
656 656 c.default_repo_data = {
657 657 'source_repo_name': source_repo.repo_name,
658 658 'source_refs_json': json.dumps(source_repo_data),
659 659 'target_repo_name': default_target_repo.repo_name,
660 660 'target_refs_json': json.dumps(target_repo_data),
661 661 }
662 662 c.default_source_ref = selected_source_ref
663 663
664 664 return self._get_template_context(c)
665 665
666 666 @LoginRequired()
667 667 @NotAnonymous()
668 668 @HasRepoPermissionAnyDecorator(
669 669 'repository.read', 'repository.write', 'repository.admin')
670 670 @view_config(
671 671 route_name='pullrequest_repo_refs', request_method='GET',
672 672 renderer='json_ext', xhr=True)
673 673 def pull_request_repo_refs(self):
674 674 target_repo_name = self.request.matchdict['target_repo_name']
675 675 repo = Repository.get_by_repo_name(target_repo_name)
676 676 if not repo:
677 677 raise HTTPNotFound()
678 678 return PullRequestModel().generate_repo_data(repo)
679 679
680 680 @LoginRequired()
681 681 @NotAnonymous()
682 682 @HasRepoPermissionAnyDecorator(
683 683 'repository.read', 'repository.write', 'repository.admin')
684 684 @view_config(
685 685 route_name='pullrequest_repo_destinations', request_method='GET',
686 686 renderer='json_ext', xhr=True)
687 687 def pull_request_repo_destinations(self):
688 688 _ = self.request.translate
689 689 filter_query = self.request.GET.get('query')
690 690
691 691 query = Repository.query() \
692 692 .order_by(func.length(Repository.repo_name)) \
693 693 .filter(
694 694 or_(Repository.repo_name == self.db_repo.repo_name,
695 695 Repository.fork_id == self.db_repo.repo_id))
696 696
697 697 if filter_query:
698 698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 699 query = query.filter(
700 700 Repository.repo_name.ilike(ilike_expression))
701 701
702 702 add_parent = False
703 703 if self.db_repo.parent:
704 704 if filter_query in self.db_repo.parent.repo_name:
705 705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 707 add_parent = True
708 708
709 709 limit = 20 - 1 if add_parent else 20
710 710 all_repos = query.limit(limit).all()
711 711 if add_parent:
712 712 all_repos += [self.db_repo.parent]
713 713
714 714 repos = []
715 715 for obj in ScmModel().get_repos(all_repos):
716 716 repos.append({
717 717 'id': obj['name'],
718 718 'text': obj['name'],
719 719 'type': 'repo',
720 720 'obj': obj['dbrepo']
721 721 })
722 722
723 723 data = {
724 724 'more': False,
725 725 'results': [{
726 726 'text': _('Repositories'),
727 727 'children': repos
728 728 }] if repos else []
729 729 }
730 730 return data
731 731
732 732 @LoginRequired()
733 733 @NotAnonymous()
734 734 @HasRepoPermissionAnyDecorator(
735 735 'repository.read', 'repository.write', 'repository.admin')
736 736 @CSRFRequired()
737 737 @view_config(
738 738 route_name='pullrequest_create', request_method='POST',
739 739 renderer=None)
740 740 def pull_request_create(self):
741 741 _ = self.request.translate
742 742 self.assure_not_empty_repo()
743 743
744 744 controls = peppercorn.parse(self.request.POST.items())
745 745
746 746 try:
747 747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 748 except formencode.Invalid as errors:
749 749 if errors.error_dict.get('revisions'):
750 750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 751 elif errors.error_dict.get('pullrequest_title'):
752 752 msg = _('Pull request requires a title with min. 3 chars')
753 753 else:
754 754 msg = _('Error creating pull request: {}').format(errors)
755 755 log.exception(msg)
756 756 h.flash(msg, 'error')
757 757
758 758 # would rather just go back to form ...
759 759 raise HTTPFound(
760 760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761 761
762 762 source_repo = _form['source_repo']
763 763 source_ref = _form['source_ref']
764 764 target_repo = _form['target_repo']
765 765 target_ref = _form['target_ref']
766 766 commit_ids = _form['revisions'][::-1]
767 767
768 768 # find the ancestor for this pr
769 769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771 771
772 772 source_scm = source_db_repo.scm_instance()
773 773 target_scm = target_db_repo.scm_instance()
774 774
775 775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777 777
778 778 ancestor = source_scm.get_common_ancestor(
779 779 source_commit.raw_id, target_commit.raw_id, target_scm)
780 780
781 781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783 783
784 784 pullrequest_title = _form['pullrequest_title']
785 785 title_source_ref = source_ref.split(':', 2)[1]
786 786 if not pullrequest_title:
787 787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 788 source=source_repo,
789 789 source_ref=title_source_ref,
790 790 target=target_repo
791 791 )
792 792
793 793 description = _form['pullrequest_desc']
794 794
795 795 get_default_reviewers_data, validate_default_reviewers = \
796 796 PullRequestModel().get_reviewer_functions()
797 797
798 798 # recalculate reviewers logic, to make sure we can validate this
799 799 reviewer_rules = get_default_reviewers_data(
800 800 self._rhodecode_db_user, source_db_repo,
801 801 source_commit, target_db_repo, target_commit)
802 802
803 803 given_reviewers = _form['review_members']
804 804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805 805
806 806 try:
807 807 pull_request = PullRequestModel().create(
808 808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 809 target_ref, commit_ids, reviewers, pullrequest_title,
810 810 description, reviewer_rules
811 811 )
812 812 Session().commit()
813 813 h.flash(_('Successfully opened new pull request'),
814 814 category='success')
815 except Exception as e:
815 except Exception:
816 816 msg = _('Error occurred during creation of this pull request.')
817 817 log.exception(msg)
818 818 h.flash(msg, category='error')
819
820 # copy the args back to redirect
821 org_query = self.request.GET.mixed()
819 822 raise HTTPFound(
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
823 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
824 _query=org_query))
821 825
822 826 raise HTTPFound(
823 827 h.route_path('pullrequest_show', repo_name=target_repo,
824 828 pull_request_id=pull_request.pull_request_id))
825 829
826 830 @LoginRequired()
827 831 @NotAnonymous()
828 832 @HasRepoPermissionAnyDecorator(
829 833 'repository.read', 'repository.write', 'repository.admin')
830 834 @CSRFRequired()
831 835 @view_config(
832 836 route_name='pullrequest_update', request_method='POST',
833 837 renderer='json_ext')
834 838 def pull_request_update(self):
835 839 pull_request = PullRequest.get_or_404(
836 840 self.request.matchdict['pull_request_id'])
837 841
838 842 # only owner or admin can update it
839 843 allowed_to_update = PullRequestModel().check_user_update(
840 844 pull_request, self._rhodecode_user)
841 845 if allowed_to_update:
842 846 controls = peppercorn.parse(self.request.POST.items())
843 847
844 848 if 'review_members' in controls:
845 849 self._update_reviewers(
846 850 pull_request, controls['review_members'],
847 851 pull_request.reviewer_data)
848 852 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 853 self._update_commits(pull_request)
850 854 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 855 self._edit_pull_request(pull_request)
852 856 else:
853 857 raise HTTPBadRequest()
854 858 return True
855 859 raise HTTPForbidden()
856 860
857 861 def _edit_pull_request(self, pull_request):
858 862 _ = self.request.translate
859 863 try:
860 864 PullRequestModel().edit(
861 865 pull_request, self.request.POST.get('title'),
862 866 self.request.POST.get('description'), self._rhodecode_user)
863 867 except ValueError:
864 868 msg = _(u'Cannot update closed pull requests.')
865 869 h.flash(msg, category='error')
866 870 return
867 871 else:
868 872 Session().commit()
869 873
870 874 msg = _(u'Pull request title & description updated.')
871 875 h.flash(msg, category='success')
872 876 return
873 877
874 878 def _update_commits(self, pull_request):
875 879 _ = self.request.translate
876 880 resp = PullRequestModel().update_commits(pull_request)
877 881
878 882 if resp.executed:
879 883
880 884 if resp.target_changed and resp.source_changed:
881 885 changed = 'target and source repositories'
882 886 elif resp.target_changed and not resp.source_changed:
883 887 changed = 'target repository'
884 888 elif not resp.target_changed and resp.source_changed:
885 889 changed = 'source repository'
886 890 else:
887 891 changed = 'nothing'
888 892
889 893 msg = _(
890 894 u'Pull request updated to "{source_commit_id}" with '
891 895 u'{count_added} added, {count_removed} removed commits. '
892 896 u'Source of changes: {change_source}')
893 897 msg = msg.format(
894 898 source_commit_id=pull_request.source_ref_parts.commit_id,
895 899 count_added=len(resp.changes.added),
896 900 count_removed=len(resp.changes.removed),
897 901 change_source=changed)
898 902 h.flash(msg, category='success')
899 903
900 904 channel = '/repo${}$/pr/{}'.format(
901 905 pull_request.target_repo.repo_name,
902 906 pull_request.pull_request_id)
903 907 message = msg + (
904 908 ' - <a onclick="window.location.reload()">'
905 909 '<strong>{}</strong></a>'.format(_('Reload page')))
906 910 channelstream.post_message(
907 911 channel, message, self._rhodecode_user.username,
908 912 registry=self.request.registry)
909 913 else:
910 914 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 915 warning_reasons = [
912 916 UpdateFailureReason.NO_CHANGE,
913 917 UpdateFailureReason.WRONG_REF_TYPE,
914 918 ]
915 919 category = 'warning' if resp.reason in warning_reasons else 'error'
916 920 h.flash(msg, category=category)
917 921
918 922 @LoginRequired()
919 923 @NotAnonymous()
920 924 @HasRepoPermissionAnyDecorator(
921 925 'repository.read', 'repository.write', 'repository.admin')
922 926 @CSRFRequired()
923 927 @view_config(
924 928 route_name='pullrequest_merge', request_method='POST',
925 929 renderer='json_ext')
926 930 def pull_request_merge(self):
927 931 """
928 932 Merge will perform a server-side merge of the specified
929 933 pull request, if the pull request is approved and mergeable.
930 934 After successful merging, the pull request is automatically
931 935 closed, with a relevant comment.
932 936 """
933 937 pull_request = PullRequest.get_or_404(
934 938 self.request.matchdict['pull_request_id'])
935 939
936 940 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 941 merge_possible = not check.failed
938 942
939 943 for err_type, error_msg in check.errors:
940 944 h.flash(error_msg, category=err_type)
941 945
942 946 if merge_possible:
943 947 log.debug("Pre-conditions checked, trying to merge.")
944 948 extras = vcs_operation_context(
945 949 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 950 username=self._rhodecode_db_user.username, action='push',
947 951 scm=pull_request.target_repo.repo_type)
948 952 self._merge_pull_request(
949 953 pull_request, self._rhodecode_db_user, extras)
950 954 else:
951 955 log.debug("Pre-conditions failed, NOT merging.")
952 956
953 957 raise HTTPFound(
954 958 h.route_path('pullrequest_show',
955 959 repo_name=pull_request.target_repo.repo_name,
956 960 pull_request_id=pull_request.pull_request_id))
957 961
958 962 def _merge_pull_request(self, pull_request, user, extras):
959 963 _ = self.request.translate
960 964 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961 965
962 966 if merge_resp.executed:
963 967 log.debug("The merge was successful, closing the pull request.")
964 968 PullRequestModel().close_pull_request(
965 969 pull_request.pull_request_id, user)
966 970 Session().commit()
967 971 msg = _('Pull request was successfully merged and closed.')
968 972 h.flash(msg, category='success')
969 973 else:
970 974 log.debug(
971 975 "The merge was not successful. Merge response: %s",
972 976 merge_resp)
973 977 msg = PullRequestModel().merge_status_message(
974 978 merge_resp.failure_reason)
975 979 h.flash(msg, category='error')
976 980
977 981 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
978 982 _ = self.request.translate
979 983 get_default_reviewers_data, validate_default_reviewers = \
980 984 PullRequestModel().get_reviewer_functions()
981 985
982 986 try:
983 987 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 988 except ValueError as e:
985 989 log.error('Reviewers Validation: {}'.format(e))
986 990 h.flash(e, category='error')
987 991 return
988 992
989 993 PullRequestModel().update_reviewers(
990 994 pull_request, reviewers, self._rhodecode_user)
991 995 h.flash(_('Pull request reviewers updated.'), category='success')
992 996 Session().commit()
993 997
994 998 @LoginRequired()
995 999 @NotAnonymous()
996 1000 @HasRepoPermissionAnyDecorator(
997 1001 'repository.read', 'repository.write', 'repository.admin')
998 1002 @CSRFRequired()
999 1003 @view_config(
1000 1004 route_name='pullrequest_delete', request_method='POST',
1001 1005 renderer='json_ext')
1002 1006 def pull_request_delete(self):
1003 1007 _ = self.request.translate
1004 1008
1005 1009 pull_request = PullRequest.get_or_404(
1006 1010 self.request.matchdict['pull_request_id'])
1007 1011
1008 1012 pr_closed = pull_request.is_closed()
1009 1013 allowed_to_delete = PullRequestModel().check_user_delete(
1010 1014 pull_request, self._rhodecode_user) and not pr_closed
1011 1015
1012 1016 # only owner can delete it !
1013 1017 if allowed_to_delete:
1014 1018 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 1019 Session().commit()
1016 1020 h.flash(_('Successfully deleted pull request'),
1017 1021 category='success')
1018 1022 raise HTTPFound(h.route_path('pullrequest_show_all',
1019 1023 repo_name=self.db_repo_name))
1020 1024
1021 1025 log.warning('user %s tried to delete pull request without access',
1022 1026 self._rhodecode_user)
1023 1027 raise HTTPNotFound()
1024 1028
1025 1029 @LoginRequired()
1026 1030 @NotAnonymous()
1027 1031 @HasRepoPermissionAnyDecorator(
1028 1032 'repository.read', 'repository.write', 'repository.admin')
1029 1033 @CSRFRequired()
1030 1034 @view_config(
1031 1035 route_name='pullrequest_comment_create', request_method='POST',
1032 1036 renderer='json_ext')
1033 1037 def pull_request_comment_create(self):
1034 1038 _ = self.request.translate
1035 1039
1036 1040 pull_request = PullRequest.get_or_404(
1037 1041 self.request.matchdict['pull_request_id'])
1038 1042 pull_request_id = pull_request.pull_request_id
1039 1043
1040 1044 if pull_request.is_closed():
1041 1045 log.debug('comment: forbidden because pull request is closed')
1042 1046 raise HTTPForbidden()
1043 1047
1044 1048 c = self.load_default_context()
1045 1049
1046 1050 status = self.request.POST.get('changeset_status', None)
1047 1051 text = self.request.POST.get('text')
1048 1052 comment_type = self.request.POST.get('comment_type')
1049 1053 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1050 1054 close_pull_request = self.request.POST.get('close_pull_request')
1051 1055
1052 1056 # the logic here should work like following, if we submit close
1053 1057 # pr comment, use `close_pull_request_with_comment` function
1054 1058 # else handle regular comment logic
1055 1059
1056 1060 if close_pull_request:
1057 1061 # only owner or admin or person with write permissions
1058 1062 allowed_to_close = PullRequestModel().check_user_update(
1059 1063 pull_request, self._rhodecode_user)
1060 1064 if not allowed_to_close:
1061 1065 log.debug('comment: forbidden because not allowed to close '
1062 1066 'pull request %s', pull_request_id)
1063 1067 raise HTTPForbidden()
1064 1068 comment, status = PullRequestModel().close_pull_request_with_comment(
1065 1069 pull_request, self._rhodecode_user, self.db_repo, message=text)
1066 1070 Session().flush()
1067 1071 events.trigger(
1068 1072 events.PullRequestCommentEvent(pull_request, comment))
1069 1073
1070 1074 else:
1071 1075 # regular comment case, could be inline, or one with status.
1072 1076 # for that one we check also permissions
1073 1077
1074 1078 allowed_to_change_status = PullRequestModel().check_user_change_status(
1075 1079 pull_request, self._rhodecode_user)
1076 1080
1077 1081 if status and allowed_to_change_status:
1078 1082 message = (_('Status change %(transition_icon)s %(status)s')
1079 1083 % {'transition_icon': '>',
1080 1084 'status': ChangesetStatus.get_status_lbl(status)})
1081 1085 text = text or message
1082 1086
1083 1087 comment = CommentsModel().create(
1084 1088 text=text,
1085 1089 repo=self.db_repo.repo_id,
1086 1090 user=self._rhodecode_user.user_id,
1087 1091 pull_request=pull_request,
1088 1092 f_path=self.request.POST.get('f_path'),
1089 1093 line_no=self.request.POST.get('line'),
1090 1094 status_change=(ChangesetStatus.get_status_lbl(status)
1091 1095 if status and allowed_to_change_status else None),
1092 1096 status_change_type=(status
1093 1097 if status and allowed_to_change_status else None),
1094 1098 comment_type=comment_type,
1095 1099 resolves_comment_id=resolves_comment_id
1096 1100 )
1097 1101
1098 1102 if allowed_to_change_status:
1099 1103 # calculate old status before we change it
1100 1104 old_calculated_status = pull_request.calculated_review_status()
1101 1105
1102 1106 # get status if set !
1103 1107 if status:
1104 1108 ChangesetStatusModel().set_status(
1105 1109 self.db_repo.repo_id,
1106 1110 status,
1107 1111 self._rhodecode_user.user_id,
1108 1112 comment,
1109 1113 pull_request=pull_request
1110 1114 )
1111 1115
1112 1116 Session().flush()
1113 1117 events.trigger(
1114 1118 events.PullRequestCommentEvent(pull_request, comment))
1115 1119
1116 1120 # we now calculate the status of pull request, and based on that
1117 1121 # calculation we set the commits status
1118 1122 calculated_status = pull_request.calculated_review_status()
1119 1123 if old_calculated_status != calculated_status:
1120 1124 PullRequestModel()._trigger_pull_request_hook(
1121 1125 pull_request, self._rhodecode_user, 'review_status_change')
1122 1126
1123 1127 Session().commit()
1124 1128
1125 1129 data = {
1126 1130 'target_id': h.safeid(h.safe_unicode(
1127 1131 self.request.POST.get('f_path'))),
1128 1132 }
1129 1133 if comment:
1130 1134 c.co = comment
1131 1135 rendered_comment = render(
1132 1136 'rhodecode:templates/changeset/changeset_comment_block.mako',
1133 1137 self._get_template_context(c), self.request)
1134 1138
1135 1139 data.update(comment.get_dict())
1136 1140 data.update({'rendered_text': rendered_comment})
1137 1141
1138 1142 return data
1139 1143
1140 1144 @LoginRequired()
1141 1145 @NotAnonymous()
1142 1146 @HasRepoPermissionAnyDecorator(
1143 1147 'repository.read', 'repository.write', 'repository.admin')
1144 1148 @CSRFRequired()
1145 1149 @view_config(
1146 1150 route_name='pullrequest_comment_delete', request_method='POST',
1147 1151 renderer='json_ext')
1148 1152 def pull_request_comment_delete(self):
1149 1153 pull_request = PullRequest.get_or_404(
1150 1154 self.request.matchdict['pull_request_id'])
1151 1155
1152 1156 comment = ChangesetComment.get_or_404(
1153 1157 self.request.matchdict['comment_id'])
1154 1158 comment_id = comment.comment_id
1155 1159
1156 1160 if pull_request.is_closed():
1157 1161 log.debug('comment: forbidden because pull request is closed')
1158 1162 raise HTTPForbidden()
1159 1163
1160 1164 if not comment:
1161 1165 log.debug('Comment with id:%s not found, skipping', comment_id)
1162 1166 # comment already deleted in another call probably
1163 1167 return True
1164 1168
1165 1169 if comment.pull_request.is_closed():
1166 1170 # don't allow deleting comments on closed pull request
1167 1171 raise HTTPForbidden()
1168 1172
1169 1173 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1170 1174 super_admin = h.HasPermissionAny('hg.admin')()
1171 1175 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1172 1176 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1173 1177 comment_repo_admin = is_repo_admin and is_repo_comment
1174 1178
1175 1179 if super_admin or comment_owner or comment_repo_admin:
1176 1180 old_calculated_status = comment.pull_request.calculated_review_status()
1177 1181 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1178 1182 Session().commit()
1179 1183 calculated_status = comment.pull_request.calculated_review_status()
1180 1184 if old_calculated_status != calculated_status:
1181 1185 PullRequestModel()._trigger_pull_request_hook(
1182 1186 comment.pull_request, self._rhodecode_user, 'review_status_change')
1183 1187 return True
1184 1188 else:
1185 1189 log.warning('No permissions for user %s to delete comment_id: %s',
1186 1190 self._rhodecode_db_user, comment_id)
1187 1191 raise HTTPNotFound()
@@ -1,526 +1,526 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name), id='pull_request_form', method='POST', request=request)}
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', method='POST', request=request)}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208 } else {
209 209 id = selectedRef;
210 210 refData = selectedRef.split(':');
211 211 }
212 212
213 213 var text = refData[1];
214 214 if (refData[0] === 'rev') {
215 215 text = text.substring(0, 12);
216 216 }
217 217
218 218 var data = {id: id, text: text};
219 219
220 220 callback(data);
221 221 };
222 222 };
223 223
224 224 var formatRefSelection = function(item) {
225 225 var prefix = '';
226 226 var refData = item.id.split(':');
227 227 if (refData[0] === 'branch') {
228 228 prefix = '<i class="icon-branch"></i>';
229 229 }
230 230 else if (refData[0] === 'book') {
231 231 prefix = '<i class="icon-bookmark"></i>';
232 232 }
233 233 else if (refData[0] === 'tag') {
234 234 prefix = '<i class="icon-tag"></i>';
235 235 }
236 236
237 237 var originalOption = item.element;
238 238 return prefix + item.text;
239 239 };
240 240
241 241 // custom code mirror
242 242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243 243
244 244 reviewersController = new ReviewersController();
245 245
246 246 var queryTargetRepo = function(self, query) {
247 247 // cache ALL results if query is empty
248 248 var cacheKey = query.term || '__';
249 249 var cachedData = self.cachedDataSource[cacheKey];
250 250
251 251 if (cachedData) {
252 252 query.callback({results: cachedData.results});
253 253 } else {
254 254 $.ajax({
255 255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 256 data: {query: query.term},
257 257 dataType: 'json',
258 258 type: 'GET',
259 259 success: function(data) {
260 260 self.cachedDataSource[cacheKey] = data;
261 261 query.callback({results: data.results});
262 262 },
263 263 error: function(data, textStatus, errorThrown) {
264 264 alert(
265 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 266 }
267 267 });
268 268 }
269 269 };
270 270
271 271 var queryTargetRefs = function(initialData, query) {
272 272 var data = {results: []};
273 273 // filter initialData
274 274 $.each(initialData, function() {
275 275 var section = this.text;
276 276 var children = [];
277 277 $.each(this.children, function() {
278 278 if (query.term.length === 0 ||
279 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 280 children.push({'id': this.id, 'text': this.text})
281 281 }
282 282 });
283 283 data.results.push({'text': section, 'children': children})
284 284 });
285 285 query.callback({results: data.results});
286 286 };
287 287
288 288 var loadRepoRefDiffPreview = function() {
289 289
290 290 var url_data = {
291 291 'repo_name': targetRepo(),
292 292 'target_repo': sourceRepo(),
293 293 'source_ref': targetRef()[2],
294 294 'source_ref_type': 'rev',
295 295 'target_ref': sourceRef()[2],
296 296 'target_ref_type': 'rev',
297 297 'merge': true,
298 298 '_': Date.now() // bypass browser caching
299 299 }; // gather the source/target ref and repo here
300 300
301 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 302 prButtonLock(true, "${_('Please select source and target')}");
303 303 return;
304 304 }
305 305 var url = pyroutes.url('repo_compare', url_data);
306 306
307 307 // lock PR button, so we cannot send PR before it's calculated
308 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309 309
310 310 if (loadRepoRefDiffPreview._currentRequest) {
311 311 loadRepoRefDiffPreview._currentRequest.abort();
312 312 }
313 313
314 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 315 .error(function(data, textStatus, errorThrown) {
316 316 alert(
317 317 "Error while processing request.\nError code {0} ({1}).".format(
318 318 data.status, data.statusText));
319 319 })
320 320 .done(function(data) {
321 321 loadRepoRefDiffPreview._currentRequest = null;
322 322 $('#pull_request_overview').html(data);
323 323
324 324 var commitElements = $(data).find('tr[commit_id]');
325 325
326 326 var prTitleAndDesc = getTitleAndDescription(
327 327 sourceRef()[1], commitElements, 5);
328 328
329 329 var title = prTitleAndDesc[0];
330 330 var proposedDescription = prTitleAndDesc[1];
331 331
332 332 var useGeneratedTitle = (
333 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 334 $('#pullrequest_title').val() === "");
335 335
336 336 if (title && useGeneratedTitle) {
337 337 // use generated title if we haven't specified our own
338 338 $('#pullrequest_title').val(title);
339 339 $('#pullrequest_title').addClass('autogenerated-title');
340 340
341 341 }
342 342
343 343 var useGeneratedDescription = (
344 344 !codeMirrorInstance._userDefinedDesc ||
345 345 codeMirrorInstance.getValue() === "");
346 346
347 347 if (proposedDescription && useGeneratedDescription) {
348 348 // set proposed content, if we haven't defined our own,
349 349 // or we don't have description written
350 350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 351 codeMirrorInstance.setValue(proposedDescription);
352 352 }
353 353
354 354 var msg = '';
355 355 if (commitElements.length === 1) {
356 356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 357 } else {
358 358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 359 }
360 360
361 361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362 362
363 363 if (commitElements.length) {
364 364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 366 }
367 367 else {
368 368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 369 }
370 370
371 371
372 372 });
373 373 };
374 374
375 375 var Select2Box = function(element, overrides) {
376 376 var globalDefaults = {
377 377 dropdownAutoWidth: true,
378 378 containerCssClass: "drop-menu",
379 379 dropdownCssClass: "drop-menu-dropdown"
380 380 };
381 381
382 382 var initSelect2 = function(defaultOptions) {
383 383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 384 element.select2(options);
385 385 };
386 386
387 387 return {
388 388 initRef: function() {
389 389 var defaultOptions = {
390 390 minimumResultsForSearch: 5,
391 391 formatSelection: formatRefSelection
392 392 };
393 393
394 394 initSelect2(defaultOptions);
395 395 },
396 396
397 397 initRepo: function(defaultValue, readOnly) {
398 398 var defaultOptions = {
399 399 initSelection : function (element, callback) {
400 400 var data = {id: defaultValue, text: defaultValue};
401 401 callback(data);
402 402 }
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406
407 407 element.select2('val', defaultSourceRepo);
408 408 if (readOnly === true) {
409 409 element.select2('readonly', true);
410 410 }
411 411 }
412 412 };
413 413 };
414 414
415 415 var initTargetRefs = function(refsData, selectedRef){
416 416 Select2Box($targetRef, {
417 417 query: function(query) {
418 418 queryTargetRefs(refsData, query);
419 419 },
420 420 initSelection : initRefSelection(selectedRef)
421 421 }).initRef();
422 422
423 423 if (!(selectedRef === undefined)) {
424 424 $targetRef.select2('val', selectedRef);
425 425 }
426 426 };
427 427
428 428 var targetRepoChanged = function(repoData) {
429 429 // generate new DESC of target repo displayed next to select
430 430 $('#target_repo_desc').html(
431 431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 432 );
433 433
434 434 // generate dynamic select2 for refs.
435 435 initTargetRefs(repoData['refs']['select2_refs'],
436 436 repoData['refs']['selected_ref']);
437 437
438 438 };
439 439
440 440 var sourceRefSelect2 = Select2Box($sourceRef, {
441 441 placeholder: "${_('Select commit reference')}",
442 442 query: function(query) {
443 443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 444 queryTargetRefs(initialData, query)
445 445 },
446 446 initSelection: initRefSelection()
447 447 }
448 448 );
449 449
450 450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 451 query: function(query) {}
452 452 });
453 453
454 454 var targetRepoSelect2 = Select2Box($targetRepo, {
455 455 cachedDataSource: {},
456 456 query: $.debounce(250, function(query) {
457 457 queryTargetRepo(this, query);
458 458 }),
459 459 formatResult: formatResult
460 460 });
461 461
462 462 sourceRefSelect2.initRef();
463 463
464 464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465 465
466 466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467 467
468 468 $sourceRef.on('change', function(e){
469 469 loadRepoRefDiffPreview();
470 470 reviewersController.loadDefaultReviewers(
471 471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 472 });
473 473
474 474 $targetRef.on('change', function(e){
475 475 loadRepoRefDiffPreview();
476 476 reviewersController.loadDefaultReviewers(
477 477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 478 });
479 479
480 480 $targetRepo.on('change', function(e){
481 481 var repoName = $(this).val();
482 482 calculateContainerWidth();
483 483 $targetRef.select2('destroy');
484 484 $('#target_ref_loading').show();
485 485
486 486 $.ajax({
487 487 url: pyroutes.url('pullrequest_repo_refs',
488 488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 489 data: {},
490 490 dataType: 'json',
491 491 type: 'GET',
492 492 success: function(data) {
493 493 $('#target_ref_loading').hide();
494 494 targetRepoChanged(data);
495 495 loadRepoRefDiffPreview();
496 496 },
497 497 error: function(data, textStatus, errorThrown) {
498 498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 499 }
500 500 })
501 501
502 502 });
503 503
504 504 prButtonLock(true, "${_('Please select source and target')}", 'all');
505 505
506 506 // auto-load on init, the target refs select2
507 507 calculateContainerWidth();
508 508 targetRepoChanged(defaultTargetRepoData);
509 509
510 510 $('#pullrequest_title').on('keyup', function(e){
511 511 $(this).removeClass('autogenerated-title');
512 512 });
513 513
514 514 % if c.default_source_ref:
515 515 // in case we have a pre-selected value, use it now
516 516 $sourceRef.select2('val', '${c.default_source_ref}');
517 517 loadRepoRefDiffPreview();
518 518 reviewersController.loadDefaultReviewers(
519 519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 520 % endif
521 521
522 522 ReviewerAutoComplete('#user');
523 523 });
524 524 </script>
525 525
526 526 </%def>
General Comments 0
You need to be logged in to leave comments. Login now