##// END OF EJS Templates
pull-requests: show common ancestor inside pull-request view....
marcink -
r1594:d24b008b default
parent child Browse files
Show More
@@ -1,1060 +1,1071 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(PullrequestsController, self).__before__()
72 72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74 74
75 75 def _extract_ordering(self, request):
76 76 column_index = safe_int(request.GET.get('order[0][column]'))
77 77 order_dir = request.GET.get('order[0][dir]', 'desc')
78 78 order_by = request.GET.get(
79 79 'columns[%s][data][sort]' % column_index, 'name_raw')
80 80 return order_by, order_dir
81 81
82 82 @LoginRequired()
83 83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
84 84 'repository.admin')
85 85 @HasAcceptedRepoType('git', 'hg')
86 86 def show_all(self, repo_name):
87 87 # filter types
88 88 c.active = 'open'
89 89 c.source = str2bool(request.GET.get('source'))
90 90 c.closed = str2bool(request.GET.get('closed'))
91 91 c.my = str2bool(request.GET.get('my'))
92 92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
93 93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
94 94 c.repo_name = repo_name
95 95
96 96 opened_by = None
97 97 if c.my:
98 98 c.active = 'my'
99 99 opened_by = [c.rhodecode_user.user_id]
100 100
101 101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
102 102 if c.closed:
103 103 c.active = 'closed'
104 104 statuses = [PullRequest.STATUS_CLOSED]
105 105
106 106 if c.awaiting_review and not c.source:
107 107 c.active = 'awaiting'
108 108 if c.source and not c.awaiting_review:
109 109 c.active = 'source'
110 110 if c.awaiting_my_review:
111 111 c.active = 'awaiting_my'
112 112
113 113 data = self._get_pull_requests_list(
114 114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
115 115 if not request.is_xhr:
116 116 c.data = json.dumps(data['data'])
117 117 c.records_total = data['recordsTotal']
118 118 return render('/pullrequests/pullrequests.mako')
119 119 else:
120 120 return json.dumps(data)
121 121
122 122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
123 123 # pagination
124 124 start = safe_int(request.GET.get('start'), 0)
125 125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
126 126 order_by, order_dir = self._extract_ordering(request)
127 127
128 128 if c.awaiting_review:
129 129 pull_requests = PullRequestModel().get_awaiting_review(
130 130 repo_name, source=c.source, opened_by=opened_by,
131 131 statuses=statuses, offset=start, length=length,
132 132 order_by=order_by, order_dir=order_dir)
133 133 pull_requests_total_count = PullRequestModel(
134 134 ).count_awaiting_review(
135 135 repo_name, source=c.source, statuses=statuses,
136 136 opened_by=opened_by)
137 137 elif c.awaiting_my_review:
138 138 pull_requests = PullRequestModel().get_awaiting_my_review(
139 139 repo_name, source=c.source, opened_by=opened_by,
140 140 user_id=c.rhodecode_user.user_id, statuses=statuses,
141 141 offset=start, length=length, order_by=order_by,
142 142 order_dir=order_dir)
143 143 pull_requests_total_count = PullRequestModel(
144 144 ).count_awaiting_my_review(
145 145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
146 146 statuses=statuses, opened_by=opened_by)
147 147 else:
148 148 pull_requests = PullRequestModel().get_all(
149 149 repo_name, source=c.source, opened_by=opened_by,
150 150 statuses=statuses, offset=start, length=length,
151 151 order_by=order_by, order_dir=order_dir)
152 152 pull_requests_total_count = PullRequestModel().count_all(
153 153 repo_name, source=c.source, statuses=statuses,
154 154 opened_by=opened_by)
155 155
156 156 from rhodecode.lib.utils import PartialRenderer
157 157 _render = PartialRenderer('data_table/_dt_elements.mako')
158 158 data = []
159 159 for pr in pull_requests:
160 160 comments = CommentsModel().get_all_comments(
161 161 c.rhodecode_db_repo.repo_id, pull_request=pr)
162 162
163 163 data.append({
164 164 'name': _render('pullrequest_name',
165 165 pr.pull_request_id, pr.target_repo.repo_name),
166 166 'name_raw': pr.pull_request_id,
167 167 'status': _render('pullrequest_status',
168 168 pr.calculated_review_status()),
169 169 'title': _render(
170 170 'pullrequest_title', pr.title, pr.description),
171 171 'description': h.escape(pr.description),
172 172 'updated_on': _render('pullrequest_updated_on',
173 173 h.datetime_to_time(pr.updated_on)),
174 174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
175 175 'created_on': _render('pullrequest_updated_on',
176 176 h.datetime_to_time(pr.created_on)),
177 177 'created_on_raw': h.datetime_to_time(pr.created_on),
178 178 'author': _render('pullrequest_author',
179 179 pr.author.full_contact, ),
180 180 'author_raw': pr.author.full_name,
181 181 'comments': _render('pullrequest_comments', len(comments)),
182 182 'comments_raw': len(comments),
183 183 'closed': pr.is_closed(),
184 184 })
185 185 # json used to render the grid
186 186 data = ({
187 187 'data': data,
188 188 'recordsTotal': pull_requests_total_count,
189 189 'recordsFiltered': pull_requests_total_count,
190 190 })
191 191 return data
192 192
193 193 @LoginRequired()
194 194 @NotAnonymous()
195 195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 196 'repository.admin')
197 197 @HasAcceptedRepoType('git', 'hg')
198 198 def index(self):
199 199 source_repo = c.rhodecode_db_repo
200 200
201 201 try:
202 202 source_repo.scm_instance().get_commit()
203 203 except EmptyRepositoryError:
204 204 h.flash(h.literal(_('There are no commits yet')),
205 205 category='warning')
206 206 redirect(url('summary_home', repo_name=source_repo.repo_name))
207 207
208 208 commit_id = request.GET.get('commit')
209 209 branch_ref = request.GET.get('branch')
210 210 bookmark_ref = request.GET.get('bookmark')
211 211
212 212 try:
213 213 source_repo_data = PullRequestModel().generate_repo_data(
214 214 source_repo, commit_id=commit_id,
215 215 branch=branch_ref, bookmark=bookmark_ref)
216 216 except CommitDoesNotExistError as e:
217 217 log.exception(e)
218 218 h.flash(_('Commit does not exist'), 'error')
219 219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
220 220
221 221 default_target_repo = source_repo
222 222
223 223 if source_repo.parent:
224 224 parent_vcs_obj = source_repo.parent.scm_instance()
225 225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
226 226 # change default if we have a parent repo
227 227 default_target_repo = source_repo.parent
228 228
229 229 target_repo_data = PullRequestModel().generate_repo_data(
230 230 default_target_repo)
231 231
232 232 selected_source_ref = source_repo_data['refs']['selected_ref']
233 233
234 234 title_source_ref = selected_source_ref.split(':', 2)[1]
235 235 c.default_title = PullRequestModel().generate_pullrequest_title(
236 236 source=source_repo.repo_name,
237 237 source_ref=title_source_ref,
238 238 target=default_target_repo.repo_name
239 239 )
240 240
241 241 c.default_repo_data = {
242 242 'source_repo_name': source_repo.repo_name,
243 243 'source_refs_json': json.dumps(source_repo_data),
244 244 'target_repo_name': default_target_repo.repo_name,
245 245 'target_refs_json': json.dumps(target_repo_data),
246 246 }
247 247 c.default_source_ref = selected_source_ref
248 248
249 249 return render('/pullrequests/pullrequest.mako')
250 250
251 251 @LoginRequired()
252 252 @NotAnonymous()
253 253 @XHRRequired()
254 254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 255 'repository.admin')
256 256 @jsonify
257 257 def get_repo_refs(self, repo_name, target_repo_name):
258 258 repo = Repository.get_by_repo_name(target_repo_name)
259 259 if not repo:
260 260 raise HTTPNotFound
261 261 return PullRequestModel().generate_repo_data(repo)
262 262
263 263 @LoginRequired()
264 264 @NotAnonymous()
265 265 @XHRRequired()
266 266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
267 267 'repository.admin')
268 268 @jsonify
269 269 def get_repo_destinations(self, repo_name):
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271 if not repo:
272 272 raise HTTPNotFound
273 273 filter_query = request.GET.get('query')
274 274
275 275 query = Repository.query() \
276 276 .order_by(func.length(Repository.repo_name)) \
277 277 .filter(or_(
278 278 Repository.repo_name == repo.repo_name,
279 279 Repository.fork_id == repo.repo_id))
280 280
281 281 if filter_query:
282 282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
283 283 query = query.filter(
284 284 Repository.repo_name.ilike(ilike_expression))
285 285
286 286 add_parent = False
287 287 if repo.parent:
288 288 if filter_query in repo.parent.repo_name:
289 289 parent_vcs_obj = repo.parent.scm_instance()
290 290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
291 291 add_parent = True
292 292
293 293 limit = 20 - 1 if add_parent else 20
294 294 all_repos = query.limit(limit).all()
295 295 if add_parent:
296 296 all_repos += [repo.parent]
297 297
298 298 repos = []
299 299 for obj in self.scm_model.get_repos(all_repos):
300 300 repos.append({
301 301 'id': obj['name'],
302 302 'text': obj['name'],
303 303 'type': 'repo',
304 304 'obj': obj['dbrepo']
305 305 })
306 306
307 307 data = {
308 308 'more': False,
309 309 'results': [{
310 310 'text': _('Repositories'),
311 311 'children': repos
312 312 }] if repos else []
313 313 }
314 314 return data
315 315
316 316 @LoginRequired()
317 317 @NotAnonymous()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 @HasAcceptedRepoType('git', 'hg')
321 321 @auth.CSRFRequired()
322 322 def create(self, repo_name):
323 323 repo = Repository.get_by_repo_name(repo_name)
324 324 if not repo:
325 325 raise HTTPNotFound
326 326
327 327 controls = peppercorn.parse(request.POST.items())
328 328
329 329 try:
330 330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
331 331 except formencode.Invalid as errors:
332 332 if errors.error_dict.get('revisions'):
333 333 msg = 'Revisions: %s' % errors.error_dict['revisions']
334 334 elif errors.error_dict.get('pullrequest_title'):
335 335 msg = _('Pull request requires a title with min. 3 chars')
336 336 else:
337 337 msg = _('Error creating pull request: {}').format(errors)
338 338 log.exception(msg)
339 339 h.flash(msg, 'error')
340 340
341 341 # would rather just go back to form ...
342 342 return redirect(url('pullrequest_home', repo_name=repo_name))
343 343
344 344 source_repo = _form['source_repo']
345 345 source_ref = _form['source_ref']
346 346 target_repo = _form['target_repo']
347 347 target_ref = _form['target_ref']
348 348 commit_ids = _form['revisions'][::-1]
349 349 reviewers = [
350 350 (r['user_id'], r['reasons']) for r in _form['review_members']]
351 351
352 352 # find the ancestor for this pr
353 353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
354 354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
355 355
356 356 source_scm = source_db_repo.scm_instance()
357 357 target_scm = target_db_repo.scm_instance()
358 358
359 359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
360 360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
361 361
362 362 ancestor = source_scm.get_common_ancestor(
363 363 source_commit.raw_id, target_commit.raw_id, target_scm)
364 364
365 365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
366 366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
367 367
368 368 pullrequest_title = _form['pullrequest_title']
369 369 title_source_ref = source_ref.split(':', 2)[1]
370 370 if not pullrequest_title:
371 371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
372 372 source=source_repo,
373 373 source_ref=title_source_ref,
374 374 target=target_repo
375 375 )
376 376
377 377 description = _form['pullrequest_desc']
378 378 try:
379 379 pull_request = PullRequestModel().create(
380 380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
381 381 target_ref, commit_ids, reviewers, pullrequest_title,
382 382 description
383 383 )
384 384 Session().commit()
385 385 h.flash(_('Successfully opened new pull request'),
386 386 category='success')
387 387 except Exception as e:
388 388 msg = _('Error occurred during sending pull request')
389 389 log.exception(msg)
390 390 h.flash(msg, category='error')
391 391 return redirect(url('pullrequest_home', repo_name=repo_name))
392 392
393 393 return redirect(url('pullrequest_show', repo_name=target_repo,
394 394 pull_request_id=pull_request.pull_request_id))
395 395
396 396 @LoginRequired()
397 397 @NotAnonymous()
398 398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 399 'repository.admin')
400 400 @auth.CSRFRequired()
401 401 @jsonify
402 402 def update(self, repo_name, pull_request_id):
403 403 pull_request_id = safe_int(pull_request_id)
404 404 pull_request = PullRequest.get_or_404(pull_request_id)
405 405 # only owner or admin can update it
406 406 allowed_to_update = PullRequestModel().check_user_update(
407 407 pull_request, c.rhodecode_user)
408 408 if allowed_to_update:
409 409 controls = peppercorn.parse(request.POST.items())
410 410
411 411 if 'review_members' in controls:
412 412 self._update_reviewers(
413 413 pull_request_id, controls['review_members'])
414 414 elif str2bool(request.POST.get('update_commits', 'false')):
415 415 self._update_commits(pull_request)
416 416 elif str2bool(request.POST.get('close_pull_request', 'false')):
417 417 self._reject_close(pull_request)
418 418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
419 419 self._edit_pull_request(pull_request)
420 420 else:
421 421 raise HTTPBadRequest()
422 422 return True
423 423 raise HTTPForbidden()
424 424
425 425 def _edit_pull_request(self, pull_request):
426 426 try:
427 427 PullRequestModel().edit(
428 428 pull_request, request.POST.get('title'),
429 429 request.POST.get('description'))
430 430 except ValueError:
431 431 msg = _(u'Cannot update closed pull requests.')
432 432 h.flash(msg, category='error')
433 433 return
434 434 else:
435 435 Session().commit()
436 436
437 437 msg = _(u'Pull request title & description updated.')
438 438 h.flash(msg, category='success')
439 439 return
440 440
441 441 def _update_commits(self, pull_request):
442 442 resp = PullRequestModel().update_commits(pull_request)
443 443
444 444 if resp.executed:
445 445 msg = _(
446 446 u'Pull request updated to "{source_commit_id}" with '
447 447 u'{count_added} added, {count_removed} removed commits.')
448 448 msg = msg.format(
449 449 source_commit_id=pull_request.source_ref_parts.commit_id,
450 450 count_added=len(resp.changes.added),
451 451 count_removed=len(resp.changes.removed))
452 452 h.flash(msg, category='success')
453 453
454 454 registry = get_current_registry()
455 455 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
456 456 channelstream_config = rhodecode_plugins.get('channelstream', {})
457 457 if channelstream_config.get('enabled'):
458 458 message = msg + (
459 459 ' - <a onclick="window.location.reload()">'
460 460 '<strong>{}</strong></a>'.format(_('Reload page')))
461 461 channel = '/repo${}$/pr/{}'.format(
462 462 pull_request.target_repo.repo_name,
463 463 pull_request.pull_request_id
464 464 )
465 465 payload = {
466 466 'type': 'message',
467 467 'user': 'system',
468 468 'exclude_users': [request.user.username],
469 469 'channel': channel,
470 470 'message': {
471 471 'message': message,
472 472 'level': 'success',
473 473 'topic': '/notifications'
474 474 }
475 475 }
476 476 channelstream_request(
477 477 channelstream_config, [payload], '/message',
478 478 raise_exc=False)
479 479 else:
480 480 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
481 481 warning_reasons = [
482 482 UpdateFailureReason.NO_CHANGE,
483 483 UpdateFailureReason.WRONG_REF_TPYE,
484 484 ]
485 485 category = 'warning' if resp.reason in warning_reasons else 'error'
486 486 h.flash(msg, category=category)
487 487
488 488 @auth.CSRFRequired()
489 489 @LoginRequired()
490 490 @NotAnonymous()
491 491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
492 492 'repository.admin')
493 493 def merge(self, repo_name, pull_request_id):
494 494 """
495 495 POST /{repo_name}/pull-request/{pull_request_id}
496 496
497 497 Merge will perform a server-side merge of the specified
498 498 pull request, if the pull request is approved and mergeable.
499 499 After successful merging, the pull request is automatically
500 500 closed, with a relevant comment.
501 501 """
502 502 pull_request_id = safe_int(pull_request_id)
503 503 pull_request = PullRequest.get_or_404(pull_request_id)
504 504 user = c.rhodecode_user
505 505
506 506 check = MergeCheck.validate(pull_request, user)
507 507 merge_possible = not check.failed
508 508
509 509 for err_type, error_msg in check.errors:
510 510 h.flash(error_msg, category=err_type)
511 511
512 512 if merge_possible:
513 513 log.debug("Pre-conditions checked, trying to merge.")
514 514 extras = vcs_operation_context(
515 515 request.environ, repo_name=pull_request.target_repo.repo_name,
516 516 username=user.username, action='push',
517 517 scm=pull_request.target_repo.repo_type)
518 518 self._merge_pull_request(pull_request, user, extras)
519 519
520 520 return redirect(url(
521 521 'pullrequest_show',
522 522 repo_name=pull_request.target_repo.repo_name,
523 523 pull_request_id=pull_request.pull_request_id))
524 524
525 525 def _merge_pull_request(self, pull_request, user, extras):
526 526 merge_resp = PullRequestModel().merge(
527 527 pull_request, user, extras=extras)
528 528
529 529 if merge_resp.executed:
530 530 log.debug("The merge was successful, closing the pull request.")
531 531 PullRequestModel().close_pull_request(
532 532 pull_request.pull_request_id, user)
533 533 Session().commit()
534 534 msg = _('Pull request was successfully merged and closed.')
535 535 h.flash(msg, category='success')
536 536 else:
537 537 log.debug(
538 538 "The merge was not successful. Merge response: %s",
539 539 merge_resp)
540 540 msg = PullRequestModel().merge_status_message(
541 541 merge_resp.failure_reason)
542 542 h.flash(msg, category='error')
543 543
544 544 def _update_reviewers(self, pull_request_id, review_members):
545 545 reviewers = [
546 546 (int(r['user_id']), r['reasons']) for r in review_members]
547 547 PullRequestModel().update_reviewers(pull_request_id, reviewers)
548 548 Session().commit()
549 549
550 550 def _reject_close(self, pull_request):
551 551 if pull_request.is_closed():
552 552 raise HTTPForbidden()
553 553
554 554 PullRequestModel().close_pull_request_with_comment(
555 555 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
556 556 Session().commit()
557 557
558 558 @LoginRequired()
559 559 @NotAnonymous()
560 560 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
561 561 'repository.admin')
562 562 @auth.CSRFRequired()
563 563 @jsonify
564 564 def delete(self, repo_name, pull_request_id):
565 565 pull_request_id = safe_int(pull_request_id)
566 566 pull_request = PullRequest.get_or_404(pull_request_id)
567 567 # only owner can delete it !
568 568 if pull_request.author.user_id == c.rhodecode_user.user_id:
569 569 PullRequestModel().delete(pull_request)
570 570 Session().commit()
571 571 h.flash(_('Successfully deleted pull request'),
572 572 category='success')
573 573 return redirect(url('my_account_pullrequests'))
574 574 raise HTTPForbidden()
575 575
576 576 def _get_pr_version(self, pull_request_id, version=None):
577 577 pull_request_id = safe_int(pull_request_id)
578 578 at_version = None
579 579
580 580 if version and version == 'latest':
581 581 pull_request_ver = PullRequest.get(pull_request_id)
582 582 pull_request_obj = pull_request_ver
583 583 _org_pull_request_obj = pull_request_obj
584 584 at_version = 'latest'
585 585 elif version:
586 586 pull_request_ver = PullRequestVersion.get_or_404(version)
587 587 pull_request_obj = pull_request_ver
588 588 _org_pull_request_obj = pull_request_ver.pull_request
589 589 at_version = pull_request_ver.pull_request_version_id
590 590 else:
591 591 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
592 592
593 593 pull_request_display_obj = PullRequest.get_pr_display_object(
594 594 pull_request_obj, _org_pull_request_obj)
595 595
596 596 return _org_pull_request_obj, pull_request_obj, \
597 597 pull_request_display_obj, at_version
598 598
599 599 def _get_diffset(
600 600 self, source_repo, source_ref_id, target_ref_id, target_commit,
601 601 source_commit, diff_limit, file_limit, display_inline_comments):
602 602 vcs_diff = PullRequestModel().get_diff(
603 603 source_repo, source_ref_id, target_ref_id)
604 604
605 605 diff_processor = diffs.DiffProcessor(
606 606 vcs_diff, format='newdiff', diff_limit=diff_limit,
607 607 file_limit=file_limit, show_full_diff=c.fulldiff)
608 608
609 609 _parsed = diff_processor.prepare()
610 610
611 611 def _node_getter(commit):
612 612 def get_node(fname):
613 613 try:
614 614 return commit.get_node(fname)
615 615 except NodeDoesNotExistError:
616 616 return None
617 617
618 618 return get_node
619 619
620 620 diffset = codeblocks.DiffSet(
621 621 repo_name=c.repo_name,
622 622 source_repo_name=c.source_repo.repo_name,
623 623 source_node_getter=_node_getter(target_commit),
624 624 target_node_getter=_node_getter(source_commit),
625 625 comments=display_inline_comments
626 626 )
627 627 diffset = diffset.render_patchset(
628 628 _parsed, target_commit.raw_id, source_commit.raw_id)
629 629
630 630 return diffset
631 631
632 632 @LoginRequired()
633 633 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
634 634 'repository.admin')
635 635 def show(self, repo_name, pull_request_id):
636 636 pull_request_id = safe_int(pull_request_id)
637 637 version = request.GET.get('version')
638 638 from_version = request.GET.get('from_version') or version
639 639 merge_checks = request.GET.get('merge_checks')
640 640 c.fulldiff = str2bool(request.GET.get('fulldiff'))
641 641
642 642 (pull_request_latest,
643 643 pull_request_at_ver,
644 644 pull_request_display_obj,
645 645 at_version) = self._get_pr_version(
646 646 pull_request_id, version=version)
647 647 pr_closed = pull_request_latest.is_closed()
648 648
649 649 if pr_closed and (version or from_version):
650 650 # not allow to browse versions
651 651 return redirect(h.url('pullrequest_show', repo_name=repo_name,
652 652 pull_request_id=pull_request_id))
653 653
654 654 versions = pull_request_display_obj.versions()
655 655
656 656 c.at_version = at_version
657 657 c.at_version_num = (at_version
658 658 if at_version and at_version != 'latest'
659 659 else None)
660 660 c.at_version_pos = ChangesetComment.get_index_from_version(
661 661 c.at_version_num, versions)
662 662
663 663 (prev_pull_request_latest,
664 664 prev_pull_request_at_ver,
665 665 prev_pull_request_display_obj,
666 666 prev_at_version) = self._get_pr_version(
667 667 pull_request_id, version=from_version)
668 668
669 669 c.from_version = prev_at_version
670 670 c.from_version_num = (prev_at_version
671 671 if prev_at_version and prev_at_version != 'latest'
672 672 else None)
673 673 c.from_version_pos = ChangesetComment.get_index_from_version(
674 674 c.from_version_num, versions)
675 675
676 676 # define if we're in COMPARE mode or VIEW at version mode
677 677 compare = at_version != prev_at_version
678 678
679 679 # pull_requests repo_name we opened it against
680 680 # ie. target_repo must match
681 681 if repo_name != pull_request_at_ver.target_repo.repo_name:
682 682 raise HTTPNotFound
683 683
684 684 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
685 685 pull_request_at_ver)
686 686
687 c.ancestor = None # empty ancestor hidden in display
688 687 c.pull_request = pull_request_display_obj
689 688 c.pull_request_latest = pull_request_latest
690 689
691 690 if compare or (at_version and not at_version == 'latest'):
692 691 c.allowed_to_change_status = False
693 692 c.allowed_to_update = False
694 693 c.allowed_to_merge = False
695 694 c.allowed_to_delete = False
696 695 c.allowed_to_comment = False
697 696 c.allowed_to_close = False
698 697 else:
699 698 c.allowed_to_change_status = PullRequestModel(). \
700 699 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
701 700 and not pr_closed
702 701
703 702 c.allowed_to_update = PullRequestModel().check_user_update(
704 703 pull_request_latest, c.rhodecode_user) and not pr_closed
705 704 c.allowed_to_merge = PullRequestModel().check_user_merge(
706 705 pull_request_latest, c.rhodecode_user) and not pr_closed
707 706 c.allowed_to_delete = PullRequestModel().check_user_delete(
708 707 pull_request_latest, c.rhodecode_user) and not pr_closed
709 708 c.allowed_to_comment = not pr_closed
710 709 c.allowed_to_close = c.allowed_to_change_status and not pr_closed
711 710
712 711 # check merge capabilities
713 712 _merge_check = MergeCheck.validate(
714 713 pull_request_latest, user=c.rhodecode_user)
715 714 c.pr_merge_errors = _merge_check.error_details
716 715 c.pr_merge_possible = not _merge_check.failed
717 716 c.pr_merge_message = _merge_check.merge_msg
718 717
719 718 c.pull_request_review_status = _merge_check.review_status
720 719 if merge_checks:
721 720 return render('/pullrequests/pullrequest_merge_checks.mako')
722 721
723 722 comments_model = CommentsModel()
724 723
725 724 # reviewers and statuses
726 725 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
727 726 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
728 727
729 728 # GENERAL COMMENTS with versions #
730 729 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
731 730 q = q.order_by(ChangesetComment.comment_id.asc())
732 731 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
733 732
734 733 # pick comments we want to render at current version
735 734 c.comment_versions = comments_model.aggregate_comments(
736 735 general_comments, versions, c.at_version_num)
737 736 c.comments = c.comment_versions[c.at_version_num]['until']
738 737
739 738 # INLINE COMMENTS with versions #
740 739 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
741 740 q = q.order_by(ChangesetComment.comment_id.asc())
742 741 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
743 742 c.inline_versions = comments_model.aggregate_comments(
744 743 inline_comments, versions, c.at_version_num, inline=True)
745 744
746 745 # inject latest version
747 746 latest_ver = PullRequest.get_pr_display_object(
748 747 pull_request_latest, pull_request_latest)
749 748
750 749 c.versions = versions + [latest_ver]
751 750
752 751 # if we use version, then do not show later comments
753 752 # than current version
754 753 display_inline_comments = collections.defaultdict(
755 754 lambda: collections.defaultdict(list))
756 755 for co in inline_comments:
757 756 if c.at_version_num:
758 757 # pick comments that are at least UPTO given version, so we
759 758 # don't render comments for higher version
760 759 should_render = co.pull_request_version_id and \
761 760 co.pull_request_version_id <= c.at_version_num
762 761 else:
763 762 # showing all, for 'latest'
764 763 should_render = True
765 764
766 765 if should_render:
767 766 display_inline_comments[co.f_path][co.line_no].append(co)
768 767
769 768 # load diff data into template context, if we use compare mode then
770 769 # diff is calculated based on changes between versions of PR
771 770
772 771 source_repo = pull_request_at_ver.source_repo
773 772 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
774 773
775 774 target_repo = pull_request_at_ver.target_repo
776 775 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
777 776
778 777 if compare:
779 778 # in compare switch the diff base to latest commit from prev version
780 779 target_ref_id = prev_pull_request_display_obj.revisions[0]
781 780
782 781 # despite opening commits for bookmarks/branches/tags, we always
783 782 # convert this to rev to prevent changes after bookmark or branch change
784 783 c.source_ref_type = 'rev'
785 784 c.source_ref = source_ref_id
786 785
787 786 c.target_ref_type = 'rev'
788 787 c.target_ref = target_ref_id
789 788
790 789 c.source_repo = source_repo
791 790 c.target_repo = target_repo
792 791
793 792 # diff_limit is the old behavior, will cut off the whole diff
794 793 # if the limit is applied otherwise will just hide the
795 794 # big files from the front-end
796 795 diff_limit = self.cut_off_limit_diff
797 796 file_limit = self.cut_off_limit_file
798 797
799 798 c.commit_ranges = []
800 799 source_commit = EmptyCommit()
801 800 target_commit = EmptyCommit()
802 801 c.missing_requirements = False
803 802
803 source_scm = source_repo.scm_instance()
804 target_scm = target_repo.scm_instance()
805
804 806 # try first shadow repo, fallback to regular repo
805 807 try:
806 808 commits_source_repo = pull_request_latest.get_shadow_repo()
807 809 except Exception:
808 810 log.debug('Failed to get shadow repo', exc_info=True)
809 commits_source_repo = source_repo.scm_instance()
811 commits_source_repo = source_scm
810 812
811 813 c.commits_source_repo = commits_source_repo
812 814 commit_cache = {}
813 815 try:
814 816 pre_load = ["author", "branch", "date", "message"]
815 817 show_revs = pull_request_at_ver.revisions
816 818 for rev in show_revs:
817 819 comm = commits_source_repo.get_commit(
818 820 commit_id=rev, pre_load=pre_load)
819 821 c.commit_ranges.append(comm)
820 822 commit_cache[comm.raw_id] = comm
821 823
822 824 target_commit = commits_source_repo.get_commit(
823 825 commit_id=safe_str(target_ref_id))
824 826 source_commit = commits_source_repo.get_commit(
825 827 commit_id=safe_str(source_ref_id))
826 828 except CommitDoesNotExistError:
827 829 pass
828 830 except RepositoryRequirementError:
829 831 log.warning(
830 832 'Failed to get all required data from repo', exc_info=True)
831 833 c.missing_requirements = True
832 834
835 c.ancestor = None # set it to None, to hide it from PR view
836
837 try:
838 ancestor_id = source_scm.get_common_ancestor(
839 source_commit.raw_id, target_commit.raw_id, target_scm)
840 c.ancestor_commit = source_scm.get_commit(ancestor_id)
841 except Exception:
842 c.ancestor_commit = None
843
833 844 c.statuses = source_repo.statuses(
834 845 [x.raw_id for x in c.commit_ranges])
835 846
836 847 # auto collapse if we have more than limit
837 848 collapse_limit = diffs.DiffProcessor._collapse_commits_over
838 849 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
839 850 c.compare_mode = compare
840 851
841 852 c.missing_commits = False
842 853 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
843 854 or source_commit == target_commit):
844 855
845 856 c.missing_commits = True
846 857 else:
847 858
848 859 c.diffset = self._get_diffset(
849 860 commits_source_repo, source_ref_id, target_ref_id,
850 861 target_commit, source_commit,
851 862 diff_limit, file_limit, display_inline_comments)
852 863
853 864 c.limited_diff = c.diffset.limited_diff
854 865
855 866 # calculate removed files that are bound to comments
856 867 comment_deleted_files = [
857 868 fname for fname in display_inline_comments
858 869 if fname not in c.diffset.file_stats]
859 870
860 871 c.deleted_files_comments = collections.defaultdict(dict)
861 872 for fname, per_line_comments in display_inline_comments.items():
862 873 if fname in comment_deleted_files:
863 874 c.deleted_files_comments[fname]['stats'] = 0
864 875 c.deleted_files_comments[fname]['comments'] = list()
865 876 for lno, comments in per_line_comments.items():
866 877 c.deleted_files_comments[fname]['comments'].extend(
867 878 comments)
868 879
869 880 # this is a hack to properly display links, when creating PR, the
870 881 # compare view and others uses different notation, and
871 882 # compare_commits.mako renders links based on the target_repo.
872 883 # We need to swap that here to generate it properly on the html side
873 884 c.target_repo = c.source_repo
874 885
875 886 c.commit_statuses = ChangesetStatus.STATUSES
876 887
877 888 c.show_version_changes = not pr_closed
878 889 if c.show_version_changes:
879 890 cur_obj = pull_request_at_ver
880 891 prev_obj = prev_pull_request_at_ver
881 892
882 893 old_commit_ids = prev_obj.revisions
883 894 new_commit_ids = cur_obj.revisions
884 895 commit_changes = PullRequestModel()._calculate_commit_id_changes(
885 896 old_commit_ids, new_commit_ids)
886 897 c.commit_changes_summary = commit_changes
887 898
888 899 # calculate the diff for commits between versions
889 900 c.commit_changes = []
890 901 mark = lambda cs, fw: list(
891 902 h.itertools.izip_longest([], cs, fillvalue=fw))
892 903 for c_type, raw_id in mark(commit_changes.added, 'a') \
893 904 + mark(commit_changes.removed, 'r') \
894 905 + mark(commit_changes.common, 'c'):
895 906
896 907 if raw_id in commit_cache:
897 908 commit = commit_cache[raw_id]
898 909 else:
899 910 try:
900 911 commit = commits_source_repo.get_commit(raw_id)
901 912 except CommitDoesNotExistError:
902 913 # in case we fail extracting still use "dummy" commit
903 914 # for display in commit diff
904 915 commit = h.AttributeDict(
905 916 {'raw_id': raw_id,
906 917 'message': 'EMPTY or MISSING COMMIT'})
907 918 c.commit_changes.append([c_type, commit])
908 919
909 920 # current user review statuses for each version
910 921 c.review_versions = {}
911 922 if c.rhodecode_user.user_id in allowed_reviewers:
912 923 for co in general_comments:
913 924 if co.author.user_id == c.rhodecode_user.user_id:
914 925 # each comment has a status change
915 926 status = co.status_change
916 927 if status:
917 928 _ver_pr = status[0].comment.pull_request_version_id
918 929 c.review_versions[_ver_pr] = status[0]
919 930
920 931 return render('/pullrequests/pullrequest_show.mako')
921 932
922 933 @LoginRequired()
923 934 @NotAnonymous()
924 935 @HasRepoPermissionAnyDecorator(
925 936 'repository.read', 'repository.write', 'repository.admin')
926 937 @auth.CSRFRequired()
927 938 @jsonify
928 939 def comment(self, repo_name, pull_request_id):
929 940 pull_request_id = safe_int(pull_request_id)
930 941 pull_request = PullRequest.get_or_404(pull_request_id)
931 942 if pull_request.is_closed():
932 943 raise HTTPForbidden()
933 944
934 945 status = request.POST.get('changeset_status', None)
935 946 text = request.POST.get('text')
936 947 comment_type = request.POST.get('comment_type')
937 948 resolves_comment_id = request.POST.get('resolves_comment_id', None)
938 949 close_pull_request = request.POST.get('close_pull_request')
939 950
940 951 close_pr = False
941 952 if close_pull_request:
942 953 close_pr = True
943 954 pull_request_review_status = pull_request.calculated_review_status()
944 955 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
945 956 # approved only if we have voting consent
946 957 status = ChangesetStatus.STATUS_APPROVED
947 958 else:
948 959 status = ChangesetStatus.STATUS_REJECTED
949 960
950 961 allowed_to_change_status = PullRequestModel().check_user_change_status(
951 962 pull_request, c.rhodecode_user)
952 963
953 964 if status and allowed_to_change_status:
954 965 message = (_('Status change %(transition_icon)s %(status)s')
955 966 % {'transition_icon': '>',
956 967 'status': ChangesetStatus.get_status_lbl(status)})
957 968 if close_pr:
958 969 message = _('Closing with') + ' ' + message
959 970 text = text or message
960 971 comm = CommentsModel().create(
961 972 text=text,
962 973 repo=c.rhodecode_db_repo.repo_id,
963 974 user=c.rhodecode_user.user_id,
964 975 pull_request=pull_request_id,
965 976 f_path=request.POST.get('f_path'),
966 977 line_no=request.POST.get('line'),
967 978 status_change=(ChangesetStatus.get_status_lbl(status)
968 979 if status and allowed_to_change_status else None),
969 980 status_change_type=(status
970 981 if status and allowed_to_change_status else None),
971 982 closing_pr=close_pr,
972 983 comment_type=comment_type,
973 984 resolves_comment_id=resolves_comment_id
974 985 )
975 986
976 987 if allowed_to_change_status:
977 988 old_calculated_status = pull_request.calculated_review_status()
978 989 # get status if set !
979 990 if status:
980 991 ChangesetStatusModel().set_status(
981 992 c.rhodecode_db_repo.repo_id,
982 993 status,
983 994 c.rhodecode_user.user_id,
984 995 comm,
985 996 pull_request=pull_request_id
986 997 )
987 998
988 999 Session().flush()
989 1000 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
990 1001 # we now calculate the status of pull request, and based on that
991 1002 # calculation we set the commits status
992 1003 calculated_status = pull_request.calculated_review_status()
993 1004 if old_calculated_status != calculated_status:
994 1005 PullRequestModel()._trigger_pull_request_hook(
995 1006 pull_request, c.rhodecode_user, 'review_status_change')
996 1007
997 1008 calculated_status_lbl = ChangesetStatus.get_status_lbl(
998 1009 calculated_status)
999 1010
1000 1011 if close_pr:
1001 1012 status_completed = (
1002 1013 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1003 1014 ChangesetStatus.STATUS_REJECTED])
1004 1015 if close_pull_request or status_completed:
1005 1016 PullRequestModel().close_pull_request(
1006 1017 pull_request_id, c.rhodecode_user)
1007 1018 else:
1008 1019 h.flash(_('Closing pull request on other statuses than '
1009 1020 'rejected or approved is forbidden. '
1010 1021 'Calculated status from all reviewers '
1011 1022 'is currently: %s') % calculated_status_lbl,
1012 1023 category='warning')
1013 1024
1014 1025 Session().commit()
1015 1026
1016 1027 if not request.is_xhr:
1017 1028 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1018 1029 pull_request_id=pull_request_id))
1019 1030
1020 1031 data = {
1021 1032 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1022 1033 }
1023 1034 if comm:
1024 1035 c.co = comm
1025 1036 c.inline_comment = True if comm.line_no else False
1026 1037 data.update(comm.get_dict())
1027 1038 data.update({'rendered_text':
1028 1039 render('changeset/changeset_comment_block.mako')})
1029 1040
1030 1041 return data
1031 1042
1032 1043 @LoginRequired()
1033 1044 @NotAnonymous()
1034 1045 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1035 1046 'repository.admin')
1036 1047 @auth.CSRFRequired()
1037 1048 @jsonify
1038 1049 def delete_comment(self, repo_name, comment_id):
1039 1050 return self._delete_comment(comment_id)
1040 1051
1041 1052 def _delete_comment(self, comment_id):
1042 1053 comment_id = safe_int(comment_id)
1043 1054 co = ChangesetComment.get_or_404(comment_id)
1044 1055 if co.pull_request.is_closed():
1045 1056 # don't allow deleting comments on closed pull request
1046 1057 raise HTTPForbidden()
1047 1058
1048 1059 is_owner = co.author.user_id == c.rhodecode_user.user_id
1049 1060 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1050 1061 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1051 1062 old_calculated_status = co.pull_request.calculated_review_status()
1052 1063 CommentsModel().delete(comment=co)
1053 1064 Session().commit()
1054 1065 calculated_status = co.pull_request.calculated_review_status()
1055 1066 if old_calculated_status != calculated_status:
1056 1067 PullRequestModel()._trigger_pull_request_hook(
1057 1068 co.pull_request, c.rhodecode_user, 'review_status_change')
1058 1069 return True
1059 1070 else:
1060 1071 raise HTTPForbidden()
@@ -1,821 +1,826 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Origin')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 <br/>
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
86 91 </div>
87 92 <div class="pr-pullinfo">
88 93 %if h.is_hg(c.pull_request.source_repo):
89 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
90 95 %elif h.is_git(c.pull_request.source_repo):
91 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
92 97 %endif
93 98 </div>
94 99 </div>
95 100 </div>
96 101 <div class="field">
97 102 <div class="label-summary">
98 103 <label>${_('Target')}:</label>
99 104 </div>
100 105 <div class="input">
101 106 <div class="pr-targetinfo">
102 107 ## branch link is only valid if it is a branch
103 108 <span class="tag">
104 109 %if c.pull_request.target_ref_parts.type == 'branch':
105 110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
106 111 %else:
107 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
108 113 %endif
109 114 </span>
110 115 <span class="clone-url">
111 116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
112 117 </span>
113 118 </div>
114 119 </div>
115 120 </div>
116 121
117 122 ## Link to the shadow repository.
118 123 <div class="field">
119 124 <div class="label-summary">
120 125 <label>${_('Merge')}:</label>
121 126 </div>
122 127 <div class="input">
123 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
124 129 <div class="pr-mergeinfo">
125 130 %if h.is_hg(c.pull_request.target_repo):
126 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
127 132 %elif h.is_git(c.pull_request.target_repo):
128 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
129 134 %endif
130 135 </div>
131 136 % else:
132 137 <div class="">
133 138 ${_('Shadow repository data not available')}.
134 139 </div>
135 140 % endif
136 141 </div>
137 142 </div>
138 143
139 144 <div class="field">
140 145 <div class="label-summary">
141 146 <label>${_('Review')}:</label>
142 147 </div>
143 148 <div class="input">
144 149 %if c.pull_request_review_status:
145 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
146 151 <span class="changeset-status-lbl tooltip">
147 152 %if c.pull_request.is_closed():
148 153 ${_('Closed')},
149 154 %endif
150 155 ${h.commit_status_lbl(c.pull_request_review_status)}
151 156 </span>
152 157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
153 158 %endif
154 159 </div>
155 160 </div>
156 161 <div class="field">
157 162 <div class="pr-description-label label-summary">
158 163 <label>${_('Description')}:</label>
159 164 </div>
160 165 <div id="pr-desc" class="input">
161 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
162 167 </div>
163 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
164 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
165 170 </div>
166 171 </div>
167 172
168 173 <div class="field">
169 174 <div class="label-summary">
170 175 <label>${_('Versions')}:</label>
171 176 </div>
172 177
173 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
174 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
175 180
176 181 <div class="pr-versions">
177 182 % if c.show_version_changes:
178 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
179 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
180 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
181 186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
182 187 data-toggle-off="${_('Hide all versions of this pull request')}">
183 188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
184 189 </a>
185 190 <table>
186 191 ## SHOW ALL VERSIONS OF PR
187 192 <% ver_pr = None %>
188 193
189 194 % for data in reversed(list(enumerate(c.versions, 1))):
190 195 <% ver_pos = data[0] %>
191 196 <% ver = data[1] %>
192 197 <% ver_pr = ver.pull_request_version_id %>
193 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
194 199
195 200 <tr class="version-pr" style="display: ${display_row}">
196 201 <td>
197 202 <code>
198 203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
199 204 </code>
200 205 </td>
201 206 <td>
202 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
203 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
204 209 </td>
205 210 <td>
206 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
207 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
208 213 </div>
209 214 </td>
210 215 <td>
211 216 % if c.at_version_num != ver_pr:
212 217 <i class="icon-comment"></i>
213 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
214 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
215 220 </code>
216 221 % endif
217 222 </td>
218 223 <td>
219 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
220 225 </td>
221 226 <td>
222 227 ${h.age_component(ver.updated_on, time_is_local=True)}
223 228 </td>
224 229 </tr>
225 230 % endfor
226 231
227 232 <tr>
228 233 <td colspan="6">
229 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
230 235 data-label-text-locked="${_('select versions to show changes')}"
231 236 data-label-text-diff="${_('show changes between versions')}"
232 237 data-label-text-show="${_('show pull request for this version')}"
233 238 >
234 239 ${_('select versions to show changes')}
235 240 </button>
236 241 </td>
237 242 </tr>
238 243
239 244 ## show comment/inline comments summary
240 245 <%def name="comments_summary()">
241 246 <tr>
242 247 <td colspan="6" class="comments-summary-td">
243 248
244 249 % if c.at_version:
245 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
246 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
247 252 ${_('Comments at this version')}:
248 253 % else:
249 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
250 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
251 256 ${_('Comments for this pull request')}:
252 257 % endif
253 258
254 259
255 260 %if general_comm_count_ver:
256 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
257 262 %else:
258 263 ${_("%d General ") % general_comm_count_ver}
259 264 %endif
260 265
261 266 %if inline_comm_count_ver:
262 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
263 268 %else:
264 269 , ${_("%d Inline") % inline_comm_count_ver}
265 270 %endif
266 271
267 272 %if outdated_comm_count_ver:
268 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
269 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
270 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
271 276 %else:
272 277 , ${_("%d Outdated") % outdated_comm_count_ver}
273 278 %endif
274 279 </td>
275 280 </tr>
276 281 </%def>
277 282 ${comments_summary()}
278 283 </table>
279 284 % else:
280 285 <div class="input">
281 286 ${_('Pull request versions not available')}.
282 287 </div>
283 288 <div>
284 289 <table>
285 290 ${comments_summary()}
286 291 </table>
287 292 </div>
288 293 % endif
289 294 </div>
290 295 </div>
291 296
292 297 <div id="pr-save" class="field" style="display: none;">
293 298 <div class="label-summary"></div>
294 299 <div class="input">
295 300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
296 301 </div>
297 302 </div>
298 303 </div>
299 304 </div>
300 305 <div>
301 306 ## AUTHOR
302 307 <div class="reviewers-title block-right">
303 308 <div class="pr-details-title">
304 309 ${_('Author')}
305 310 </div>
306 311 </div>
307 312 <div class="block-right pr-details-content reviewers">
308 313 <ul class="group_members">
309 314 <li>
310 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
311 316 </li>
312 317 </ul>
313 318 </div>
314 319 ## REVIEWERS
315 320 <div class="reviewers-title block-right">
316 321 <div class="pr-details-title">
317 322 ${_('Pull request reviewers')}
318 323 %if c.allowed_to_update:
319 324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
320 325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
321 326 %endif
322 327 </div>
323 328 </div>
324 329 <div id="reviewers" class="block-right pr-details-content reviewers">
325 330 ## members goes here !
326 331 <input type="hidden" name="__start__" value="review_members:sequence">
327 332 <ul id="review_members" class="group_members">
328 333 %for member,reasons,status in c.pull_request_reviewers:
329 334 <li id="reviewer_${member.user_id}">
330 335 <div class="reviewers_member">
331 336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
332 337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
333 338 </div>
334 339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
335 340 ${self.gravatar_with_user(member.email, 16)}
336 341 </div>
337 342 <input type="hidden" name="__start__" value="reviewer:mapping">
338 343 <input type="hidden" name="__start__" value="reasons:sequence">
339 344 %for reason in reasons:
340 345 <div class="reviewer_reason">- ${reason}</div>
341 346 <input type="hidden" name="reason" value="${reason}">
342 347
343 348 %endfor
344 349 <input type="hidden" name="__end__" value="reasons:sequence">
345 350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
346 351 <input type="hidden" name="__end__" value="reviewer:mapping">
347 352 %if c.allowed_to_update:
348 353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
349 354 <i class="icon-remove-sign" ></i>
350 355 </div>
351 356 %endif
352 357 </div>
353 358 </li>
354 359 %endfor
355 360 </ul>
356 361 <input type="hidden" name="__end__" value="review_members:sequence">
357 362 %if not c.pull_request.is_closed():
358 363 <div id="add_reviewer_input" class='ac' style="display: none;">
359 364 %if c.allowed_to_update:
360 365 <div class="reviewer_ac">
361 366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
362 367 <div id="reviewers_container"></div>
363 368 </div>
364 369 <div>
365 370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
366 371 </div>
367 372 %endif
368 373 </div>
369 374 %endif
370 375 </div>
371 376 </div>
372 377 </div>
373 378 <div class="box">
374 379 ##DIFF
375 380 <div class="table" >
376 381 <div id="changeset_compare_view_content">
377 382 ##CS
378 383 % if c.missing_requirements:
379 384 <div class="box">
380 385 <div class="alert alert-warning">
381 386 <div>
382 387 <strong>${_('Missing requirements:')}</strong>
383 388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
384 389 </div>
385 390 </div>
386 391 </div>
387 392 % elif c.missing_commits:
388 393 <div class="box">
389 394 <div class="alert alert-warning">
390 395 <div>
391 396 <strong>${_('Missing commits')}:</strong>
392 397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
393 398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
394 399 </div>
395 400 </div>
396 401 </div>
397 402 % endif
398 403
399 404 <div class="compare_view_commits_title">
400 405 % if not c.compare_mode:
401 406
402 407 % if c.at_version_pos:
403 408 <h4>
404 409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
405 410 </h4>
406 411 % endif
407 412
408 413 <div class="pull-left">
409 414 <div class="btn-group">
410 415 <a
411 416 class="btn"
412 417 href="#"
413 418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
414 419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
415 420 </a>
416 421 <a
417 422 class="btn"
418 423 href="#"
419 424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
420 425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
421 426 </a>
422 427 </div>
423 428 </div>
424 429
425 430 <div class="pull-right">
426 431 % if c.allowed_to_update and not c.pull_request.is_closed():
427 432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
428 433 % else:
429 434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
430 435 % endif
431 436
432 437 </div>
433 438 % endif
434 439 </div>
435 440
436 441 % if not c.missing_commits:
437 442 % if c.compare_mode:
438 443 % if c.at_version:
439 444 <h4>
440 445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
441 446 </h4>
442 447
443 448 <div class="subtitle-compare">
444 449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
445 450 </div>
446 451
447 452 <div class="container">
448 453 <table class="rctable compare_view_commits">
449 454 <tr>
450 455 <th></th>
451 456 <th>${_('Time')}</th>
452 457 <th>${_('Author')}</th>
453 458 <th>${_('Commit')}</th>
454 459 <th></th>
455 460 <th>${_('Description')}</th>
456 461 </tr>
457 462
458 463 % for c_type, commit in c.commit_changes:
459 464 % if c_type in ['a', 'r']:
460 465 <%
461 466 if c_type == 'a':
462 467 cc_title = _('Commit added in displayed changes')
463 468 elif c_type == 'r':
464 469 cc_title = _('Commit removed in displayed changes')
465 470 else:
466 471 cc_title = ''
467 472 %>
468 473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
469 474 <td>
470 475 <div class="commit-change-indicator color-${c_type}-border">
471 476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
472 477 ${c_type.upper()}
473 478 </div>
474 479 </div>
475 480 </td>
476 481 <td class="td-time">
477 482 ${h.age_component(commit.date)}
478 483 </td>
479 484 <td class="td-user">
480 485 ${base.gravatar_with_user(commit.author, 16)}
481 486 </td>
482 487 <td class="td-hash">
483 488 <code>
484 489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
485 490 r${commit.revision}:${h.short_id(commit.raw_id)}
486 491 </a>
487 492 ${h.hidden('revisions', commit.raw_id)}
488 493 </code>
489 494 </td>
490 495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
491 496 <div class="show_more_col">
492 497 <i class="show_more"></i>
493 498 </div>
494 499 </td>
495 500 <td class="mid td-description">
496 501 <div class="log-container truncate-wrap">
497 502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
498 503 ${h.urlify_commit_message(commit.message, c.repo_name)}
499 504 </div>
500 505 </div>
501 506 </td>
502 507 </tr>
503 508 % endif
504 509 % endfor
505 510 </table>
506 511 </div>
507 512
508 513 <script>
509 514 $('.expand_commit').on('click',function(e){
510 515 var target_expand = $(this);
511 516 var cid = target_expand.data('commitId');
512 517
513 518 if (target_expand.hasClass('open')){
514 519 $('#c-'+cid).css({
515 520 'height': '1.5em',
516 521 'white-space': 'nowrap',
517 522 'text-overflow': 'ellipsis',
518 523 'overflow':'hidden'
519 524 });
520 525 target_expand.removeClass('open');
521 526 }
522 527 else {
523 528 $('#c-'+cid).css({
524 529 'height': 'auto',
525 530 'white-space': 'pre-line',
526 531 'text-overflow': 'initial',
527 532 'overflow':'visible'
528 533 });
529 534 target_expand.addClass('open');
530 535 }
531 536 });
532 537 </script>
533 538
534 539 % endif
535 540
536 541 % else:
537 542 <%include file="/compare/compare_commits.mako" />
538 543 % endif
539 544
540 545 <div class="cs_files">
541 546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
542 547 ${cbdiffs.render_diffset_menu()}
543 548 ${cbdiffs.render_diffset(
544 549 c.diffset, use_comments=True,
545 550 collapse_when_files_over=30,
546 551 disable_new_comments=not c.allowed_to_comment,
547 552 deleted_files_comments=c.deleted_files_comments)}
548 553 </div>
549 554 % else:
550 555 ## skipping commits we need to clear the view for missing commits
551 556 <div style="clear:both;"></div>
552 557 % endif
553 558
554 559 </div>
555 560 </div>
556 561
557 562 ## template for inline comment form
558 563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
559 564
560 565 ## render general comments
561 566
562 567 <div id="comment-tr-show">
563 568 <div class="comment">
564 569 % if general_outdated_comm_count_ver:
565 570 <div class="meta">
566 571 % if general_outdated_comm_count_ver == 1:
567 572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
568 573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
569 574 % else:
570 575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
571 576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
572 577 % endif
573 578 </div>
574 579 % endif
575 580 </div>
576 581 </div>
577 582
578 583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
579 584
580 585 % if not c.pull_request.is_closed():
581 586 ## merge status, and merge action
582 587 <div class="pull-request-merge">
583 588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
584 589 </div>
585 590
586 591 ## main comment form and it status
587 592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
588 593 pull_request_id=c.pull_request.pull_request_id),
589 594 c.pull_request_review_status,
590 595 is_pull_request=True, change_status=c.allowed_to_change_status)}
591 596 %endif
592 597
593 598 <script type="text/javascript">
594 599 if (location.hash) {
595 600 var result = splitDelimitedHash(location.hash);
596 601 var line = $('html').find(result.loc);
597 602 // show hidden comments if we use location.hash
598 603 if (line.hasClass('comment-general')) {
599 604 $(line).show();
600 605 } else if (line.hasClass('comment-inline')) {
601 606 $(line).show();
602 607 var $cb = $(line).closest('.cb');
603 608 $cb.removeClass('cb-collapsed')
604 609 }
605 610 if (line.length > 0){
606 611 offsetScroll(line, 70);
607 612 }
608 613 }
609 614
610 615 versionController = new VersionController();
611 616 versionController.init();
612 617
613 618
614 619 $(function(){
615 620 ReviewerAutoComplete('user');
616 621 // custom code mirror
617 622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
618 623
619 624 var PRDetails = {
620 625 editButton: $('#open_edit_pullrequest'),
621 626 closeButton: $('#close_edit_pullrequest'),
622 627 deleteButton: $('#delete_pullrequest'),
623 628 viewFields: $('#pr-desc, #pr-title'),
624 629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
625 630
626 631 init: function() {
627 632 var that = this;
628 633 this.editButton.on('click', function(e) { that.edit(); });
629 634 this.closeButton.on('click', function(e) { that.view(); });
630 635 },
631 636
632 637 edit: function(event) {
633 638 this.viewFields.hide();
634 639 this.editButton.hide();
635 640 this.deleteButton.hide();
636 641 this.closeButton.show();
637 642 this.editFields.show();
638 643 codeMirrorInstance.refresh();
639 644 },
640 645
641 646 view: function(event) {
642 647 this.editButton.show();
643 648 this.deleteButton.show();
644 649 this.editFields.hide();
645 650 this.closeButton.hide();
646 651 this.viewFields.show();
647 652 }
648 653 };
649 654
650 655 var ReviewersPanel = {
651 656 editButton: $('#open_edit_reviewers'),
652 657 closeButton: $('#close_edit_reviewers'),
653 658 addButton: $('#add_reviewer_input'),
654 659 removeButtons: $('.reviewer_member_remove'),
655 660
656 661 init: function() {
657 662 var that = this;
658 663 this.editButton.on('click', function(e) { that.edit(); });
659 664 this.closeButton.on('click', function(e) { that.close(); });
660 665 },
661 666
662 667 edit: function(event) {
663 668 this.editButton.hide();
664 669 this.closeButton.show();
665 670 this.addButton.show();
666 671 this.removeButtons.css('visibility', 'visible');
667 672 },
668 673
669 674 close: function(event) {
670 675 this.editButton.show();
671 676 this.closeButton.hide();
672 677 this.addButton.hide();
673 678 this.removeButtons.css('visibility', 'hidden');
674 679 }
675 680 };
676 681
677 682 PRDetails.init();
678 683 ReviewersPanel.init();
679 684
680 685 showOutdated = function(self){
681 686 $('.comment-inline.comment-outdated').show();
682 687 $('.filediff-outdated').show();
683 688 $('.showOutdatedComments').hide();
684 689 $('.hideOutdatedComments').show();
685 690 };
686 691
687 692 hideOutdated = function(self){
688 693 $('.comment-inline.comment-outdated').hide();
689 694 $('.filediff-outdated').hide();
690 695 $('.hideOutdatedComments').hide();
691 696 $('.showOutdatedComments').show();
692 697 };
693 698
694 699 refreshMergeChecks = function(){
695 700 var loadUrl = "${h.url.current(merge_checks=1)}";
696 701 $('.pull-request-merge').css('opacity', 0.3);
697 702 $('.action-buttons-extra').css('opacity', 0.3);
698 703
699 704 $('.pull-request-merge').load(
700 705 loadUrl, function() {
701 706 $('.pull-request-merge').css('opacity', 1);
702 707
703 708 $('.action-buttons-extra').css('opacity', 1);
704 709 injectCloseAction();
705 710 }
706 711 );
707 712 };
708 713
709 714 injectCloseAction = function() {
710 715 var closeAction = $('#close-pull-request-action').html();
711 716 var $actionButtons = $('.action-buttons-extra');
712 717 // clear the action before
713 718 $actionButtons.html("");
714 719 $actionButtons.html(closeAction);
715 720 };
716 721
717 722 closePullRequest = function (status) {
718 723 // inject closing flag
719 724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
720 725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
721 726 $(generalCommentForm.submitForm).submit();
722 727 };
723 728
724 729 $('#show-outdated-comments').on('click', function(e){
725 730 var button = $(this);
726 731 var outdated = $('.comment-outdated');
727 732
728 733 if (button.html() === "(Show)") {
729 734 button.html("(Hide)");
730 735 outdated.show();
731 736 } else {
732 737 button.html("(Show)");
733 738 outdated.hide();
734 739 }
735 740 });
736 741
737 742 $('.show-inline-comments').on('change', function(e){
738 743 var show = 'none';
739 744 var target = e.currentTarget;
740 745 if(target.checked){
741 746 show = ''
742 747 }
743 748 var boxid = $(target).attr('id_for');
744 749 var comments = $('#{0} .inline-comments'.format(boxid));
745 750 var fn_display = function(idx){
746 751 $(this).css('display', show);
747 752 };
748 753 $(comments).each(fn_display);
749 754 var btns = $('#{0} .inline-comments-button'.format(boxid));
750 755 $(btns).each(fn_display);
751 756 });
752 757
753 758 $('#merge_pull_request_form').submit(function() {
754 759 if (!$('#merge_pull_request').attr('disabled')) {
755 760 $('#merge_pull_request').attr('disabled', 'disabled');
756 761 }
757 762 return true;
758 763 });
759 764
760 765 $('#edit_pull_request').on('click', function(e){
761 766 var title = $('#pr-title-input').val();
762 767 var description = codeMirrorInstance.getValue();
763 768 editPullRequest(
764 769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
765 770 title, description);
766 771 });
767 772
768 773 $('#update_pull_request').on('click', function(e){
769 774 $(this).attr('disabled', 'disabled');
770 775 $(this).addClass('disabled');
771 776 $(this).html(_gettext('saving...'));
772 777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
773 778 });
774 779
775 780 $('#update_commits').on('click', function(e){
776 781 var isDisabled = !$(e.currentTarget).attr('disabled');
777 782 $(e.currentTarget).text(_gettext('Updating...'));
778 783 $(e.currentTarget).attr('disabled', 'disabled');
779 784 if(isDisabled){
780 785 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
781 786 }
782 787
783 788 });
784 789 // fixing issue with caches on firefox
785 790 $('#update_commits').removeAttr("disabled");
786 791
787 792 $('#close_pull_request').on('click', function(e){
788 793 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
789 794 });
790 795
791 796 $('.show-inline-comments').on('click', function(e){
792 797 var boxid = $(this).attr('data-comment-id');
793 798 var button = $(this);
794 799
795 800 if(button.hasClass("comments-visible")) {
796 801 $('#{0} .inline-comments'.format(boxid)).each(function(index){
797 802 $(this).hide();
798 803 });
799 804 button.removeClass("comments-visible");
800 805 } else {
801 806 $('#{0} .inline-comments'.format(boxid)).each(function(index){
802 807 $(this).show();
803 808 });
804 809 button.addClass("comments-visible");
805 810 }
806 811 });
807 812
808 813 // register submit callback on commentForm form to track TODOs
809 814 window.commentFormGlobalSubmitSuccessCallback = function(){
810 815 refreshMergeChecks();
811 816 };
812 817 // initial injection
813 818 injectCloseAction();
814 819
815 820 })
816 821 </script>
817 822
818 823 </div>
819 824 </div>
820 825
821 826 </%def>
General Comments 0
You need to be logged in to leave comments. Login now