##// END OF EJS Templates
comments: counts exclude now draft comments, they shouldn't be visible.
milka -
r4553:e8a9a30e default
parent child Browse files
Show More
@@ -1,822 +1,822 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29 from pyramid.view import view_config
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, NotAnonymous, CSRFRequired,
38 38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 39 from rhodecode.lib.channelstream import (
40 40 channelstream_request, ChannelstreamException)
41 41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43 from rhodecode.model.comment import CommentsModel
44 44 from rhodecode.model.db import (
45 45 IntegrityError, or_, in_filter_generator,
46 46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 47 PullRequest, UserBookmark, RepoGroup)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.model.validation_schema.schemas import user_schema
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class MyAccountView(BaseAppView, DataGridAppView):
58 58 ALLOW_SCOPED_TOKENS = False
59 59 """
60 60 This view has alternative version inside EE, if modified please take a look
61 61 in there as well.
62 62 """
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 c.user = c.auth_user.get_instance()
67 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 @view_config(
74 74 route_name='my_account_profile', request_method='GET',
75 75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 76 def my_account_profile(self):
77 77 c = self.load_default_context()
78 78 c.active = 'profile'
79 79 c.extern_type = c.user.extern_type
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 @view_config(
85 85 route_name='my_account_password', request_method='GET',
86 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 87 def my_account_password(self):
88 88 c = self.load_default_context()
89 89 c.active = 'password'
90 90 c.extern_type = c.user.extern_type
91 91
92 92 schema = user_schema.ChangePasswordSchema().bind(
93 93 username=c.user.username)
94 94
95 95 form = forms.Form(
96 96 schema,
97 97 action=h.route_path('my_account_password_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 c.form = form
101 101 return self._get_template_context(c)
102 102
103 103 @LoginRequired()
104 104 @NotAnonymous()
105 105 @CSRFRequired()
106 106 @view_config(
107 107 route_name='my_account_password_update', request_method='POST',
108 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 109 def my_account_password_update(self):
110 110 _ = self.request.translate
111 111 c = self.load_default_context()
112 112 c.active = 'password'
113 113 c.extern_type = c.user.extern_type
114 114
115 115 schema = user_schema.ChangePasswordSchema().bind(
116 116 username=c.user.username)
117 117
118 118 form = forms.Form(
119 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 120
121 121 if c.extern_type != 'rhodecode':
122 122 raise HTTPFound(self.request.route_path('my_account_password'))
123 123
124 124 controls = self.request.POST.items()
125 125 try:
126 126 valid_data = form.validate(controls)
127 127 UserModel().update_user(c.user.user_id, **valid_data)
128 128 c.user.update_userdata(force_password_change=False)
129 129 Session().commit()
130 130 except forms.ValidationFailure as e:
131 131 c.form = e
132 132 return self._get_template_context(c)
133 133
134 134 except Exception:
135 135 log.exception("Exception updating password")
136 136 h.flash(_('Error occurred during update of user password'),
137 137 category='error')
138 138 else:
139 139 instance = c.auth_user.get_instance()
140 140 self.session.setdefault('rhodecode_user', {}).update(
141 141 {'password': md5(instance.password)})
142 142 self.session.save()
143 143 h.flash(_("Successfully updated password"), category='success')
144 144
145 145 raise HTTPFound(self.request.route_path('my_account_password'))
146 146
147 147 @LoginRequired()
148 148 @NotAnonymous()
149 149 @view_config(
150 150 route_name='my_account_auth_tokens', request_method='GET',
151 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 152 def my_account_auth_tokens(self):
153 153 _ = self.request.translate
154 154
155 155 c = self.load_default_context()
156 156 c.active = 'auth_tokens'
157 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 158 c.role_values = [
159 159 (x, AuthTokenModel.cls._get_role_name(x))
160 160 for x in AuthTokenModel.cls.ROLES]
161 161 c.role_options = [(c.role_values, _("Role"))]
162 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 163 c.user.user_id, show_expired=True)
164 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 165 return self._get_template_context(c)
166 166
167 167 @LoginRequired()
168 168 @NotAnonymous()
169 169 @CSRFRequired()
170 170 @view_config(
171 171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 172 renderer='json_ext')
173 173 def my_account_auth_tokens_view(self):
174 174 _ = self.request.translate
175 175 c = self.load_default_context()
176 176
177 177 auth_token_id = self.request.POST.get('auth_token_id')
178 178
179 179 if auth_token_id:
180 180 token = UserApiKeys.get_or_404(auth_token_id)
181 181 if token.user.user_id != c.user.user_id:
182 182 raise HTTPNotFound()
183 183
184 184 return {
185 185 'auth_token': token.api_key
186 186 }
187 187
188 188 def maybe_attach_token_scope(self, token):
189 189 # implemented in EE edition
190 190 pass
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 @CSRFRequired()
195 195 @view_config(
196 196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 197 def my_account_auth_tokens_add(self):
198 198 _ = self.request.translate
199 199 c = self.load_default_context()
200 200
201 201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 202 description = self.request.POST.get('description')
203 203 role = self.request.POST.get('role')
204 204
205 205 token = UserModel().add_auth_token(
206 206 user=c.user.user_id,
207 207 lifetime_minutes=lifetime, role=role, description=description,
208 208 scope_callback=self.maybe_attach_token_scope)
209 209 token_data = token.get_api_data()
210 210
211 211 audit_logger.store_web(
212 212 'user.edit.token.add', action_data={
213 213 'data': {'token': token_data, 'user': 'self'}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 219
220 220 @LoginRequired()
221 221 @NotAnonymous()
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 225 def my_account_auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 del_auth_token = self.request.POST.get('del_auth_token')
230 230
231 231 if del_auth_token:
232 232 token = UserApiKeys.get_or_404(del_auth_token)
233 233 token_data = token.get_api_data()
234 234
235 235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 236 audit_logger.store_web(
237 237 'user.edit.token.delete', action_data={
238 238 'data': {'token': token_data, 'user': 'self'}},
239 239 user=self._rhodecode_user,)
240 240 Session().commit()
241 241 h.flash(_("Auth token successfully deleted"), category='success')
242 242
243 243 return HTTPFound(h.route_path('my_account_auth_tokens'))
244 244
245 245 @LoginRequired()
246 246 @NotAnonymous()
247 247 @view_config(
248 248 route_name='my_account_emails', request_method='GET',
249 249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 250 def my_account_emails(self):
251 251 _ = self.request.translate
252 252
253 253 c = self.load_default_context()
254 254 c.active = 'emails'
255 255
256 256 c.user_email_map = UserEmailMap.query()\
257 257 .filter(UserEmailMap.user == c.user).all()
258 258
259 259 schema = user_schema.AddEmailSchema().bind(
260 260 username=c.user.username, user_emails=c.user.emails)
261 261
262 262 form = forms.RcForm(schema,
263 263 action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 c.form = form
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @NotAnonymous()
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='my_account_emails_add', request_method='POST',
274 274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 275 def my_account_emails_add(self):
276 276 _ = self.request.translate
277 277 c = self.load_default_context()
278 278 c.active = 'emails'
279 279
280 280 schema = user_schema.AddEmailSchema().bind(
281 281 username=c.user.username, user_emails=c.user.emails)
282 282
283 283 form = forms.RcForm(
284 284 schema, action=h.route_path('my_account_emails_add'),
285 285 buttons=(forms.buttons.save, forms.buttons.reset))
286 286
287 287 controls = self.request.POST.items()
288 288 try:
289 289 valid_data = form.validate(controls)
290 290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 291 audit_logger.store_web(
292 292 'user.edit.email.add', action_data={
293 293 'data': {'email': valid_data['email'], 'user': 'self'}},
294 294 user=self._rhodecode_user,)
295 295 Session().commit()
296 296 except formencode.Invalid as error:
297 297 h.flash(h.escape(error.error_dict['email']), category='error')
298 298 except forms.ValidationFailure as e:
299 299 c.user_email_map = UserEmailMap.query() \
300 300 .filter(UserEmailMap.user == c.user).all()
301 301 c.form = e
302 302 return self._get_template_context(c)
303 303 except Exception:
304 304 log.exception("Exception adding email")
305 305 h.flash(_('Error occurred during adding email'),
306 306 category='error')
307 307 else:
308 308 h.flash(_("Successfully added email"), category='success')
309 309
310 310 raise HTTPFound(self.request.route_path('my_account_emails'))
311 311
312 312 @LoginRequired()
313 313 @NotAnonymous()
314 314 @CSRFRequired()
315 315 @view_config(
316 316 route_name='my_account_emails_delete', request_method='POST')
317 317 def my_account_emails_delete(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320
321 321 del_email_id = self.request.POST.get('del_email_id')
322 322 if del_email_id:
323 323 email = UserEmailMap.get_or_404(del_email_id).email
324 324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 325 audit_logger.store_web(
326 326 'user.edit.email.delete', action_data={
327 327 'data': {'email': email, 'user': 'self'}},
328 328 user=self._rhodecode_user,)
329 329 Session().commit()
330 330 h.flash(_("Email successfully deleted"),
331 331 category='success')
332 332 return HTTPFound(h.route_path('my_account_emails'))
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='my_account_notifications_test_channelstream',
339 339 request_method='POST', renderer='json_ext')
340 340 def my_account_notifications_test_channelstream(self):
341 341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 342 self._rhodecode_user.username, datetime.datetime.now())
343 343 payload = {
344 344 # 'channel': 'broadcast',
345 345 'type': 'message',
346 346 'timestamp': datetime.datetime.utcnow(),
347 347 'user': 'system',
348 348 'pm_users': [self._rhodecode_user.username],
349 349 'message': {
350 350 'message': message,
351 351 'level': 'info',
352 352 'topic': '/notifications'
353 353 }
354 354 }
355 355
356 356 registry = self.request.registry
357 357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 358 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 359
360 360 try:
361 361 channelstream_request(channelstream_config, [payload], '/message')
362 362 except ChannelstreamException as e:
363 363 log.exception('Failed to send channelstream data')
364 364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 365 return {"response": 'Channelstream data sent. '
366 366 'You should see a new live message now.'}
367 367
368 368 def _load_my_repos_data(self, watched=False):
369 369
370 370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371 371
372 372 if watched:
373 373 # repos user watch
374 374 repo_list = Session().query(
375 375 Repository
376 376 ) \
377 377 .join(
378 378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 379 ) \
380 380 .filter(
381 381 UserFollowing.user_id == self._rhodecode_user.user_id
382 382 ) \
383 383 .filter(or_(
384 384 # generate multiple IN to fix limitation problems
385 385 *in_filter_generator(Repository.repo_id, allowed_ids))
386 386 ) \
387 387 .order_by(Repository.repo_name) \
388 388 .all()
389 389
390 390 else:
391 391 # repos user is owner of
392 392 repo_list = Session().query(
393 393 Repository
394 394 ) \
395 395 .filter(
396 396 Repository.user_id == self._rhodecode_user.user_id
397 397 ) \
398 398 .filter(or_(
399 399 # generate multiple IN to fix limitation problems
400 400 *in_filter_generator(Repository.repo_id, allowed_ids))
401 401 ) \
402 402 .order_by(Repository.repo_name) \
403 403 .all()
404 404
405 405 _render = self.request.get_partial_renderer(
406 406 'rhodecode:templates/data_table/_dt_elements.mako')
407 407
408 408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 410 short_name=False, admin=False)
411 411
412 412 repos_data = []
413 413 for repo in repo_list:
414 414 row = {
415 415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 416 repo.private, repo.archived, repo.fork),
417 417 "name_raw": repo.repo_name.lower(),
418 418 }
419 419
420 420 repos_data.append(row)
421 421
422 422 # json used to render the grid
423 423 return json.dumps(repos_data)
424 424
425 425 @LoginRequired()
426 426 @NotAnonymous()
427 427 @view_config(
428 428 route_name='my_account_repos', request_method='GET',
429 429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 430 def my_account_repos(self):
431 431 c = self.load_default_context()
432 432 c.active = 'repos'
433 433
434 434 # json used to render the grid
435 435 c.data = self._load_my_repos_data()
436 436 return self._get_template_context(c)
437 437
438 438 @LoginRequired()
439 439 @NotAnonymous()
440 440 @view_config(
441 441 route_name='my_account_watched', request_method='GET',
442 442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 443 def my_account_watched(self):
444 444 c = self.load_default_context()
445 445 c.active = 'watched'
446 446
447 447 # json used to render the grid
448 448 c.data = self._load_my_repos_data(watched=True)
449 449 return self._get_template_context(c)
450 450
451 451 @LoginRequired()
452 452 @NotAnonymous()
453 453 @view_config(
454 454 route_name='my_account_bookmarks', request_method='GET',
455 455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 456 def my_account_bookmarks(self):
457 457 c = self.load_default_context()
458 458 c.active = 'bookmarks'
459 459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 460 self._rhodecode_db_user.user_id, cache=False)
461 461 return self._get_template_context(c)
462 462
463 463 def _process_bookmark_entry(self, entry, user_id):
464 464 position = safe_int(entry.get('position'))
465 465 cur_position = safe_int(entry.get('cur_position'))
466 466 if position is None:
467 467 return
468 468
469 469 # check if this is an existing entry
470 470 is_new = False
471 471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472 472
473 473 if db_entry and str2bool(entry.get('remove')):
474 474 log.debug('Marked bookmark %s for deletion', db_entry)
475 475 Session().delete(db_entry)
476 476 return
477 477
478 478 if not db_entry:
479 479 # new
480 480 db_entry = UserBookmark()
481 481 is_new = True
482 482
483 483 should_save = False
484 484 default_redirect_url = ''
485 485
486 486 # save repo
487 487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 488 repo = Repository.get(entry['bookmark_repo'])
489 489 perm_check = HasRepoPermissionAny(
490 490 'repository.read', 'repository.write', 'repository.admin')
491 491 if repo and perm_check(repo_name=repo.repo_name):
492 492 db_entry.repository = repo
493 493 should_save = True
494 494 default_redirect_url = '${repo_url}'
495 495 # save repo group
496 496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 498 perm_check = HasRepoGroupPermissionAny(
499 499 'group.read', 'group.write', 'group.admin')
500 500
501 501 if repo_group and perm_check(group_name=repo_group.group_name):
502 502 db_entry.repository_group = repo_group
503 503 should_save = True
504 504 default_redirect_url = '${repo_group_url}'
505 505 # save generic info
506 506 elif entry.get('title') and entry.get('redirect_url'):
507 507 should_save = True
508 508
509 509 if should_save:
510 510 # mark user and position
511 511 db_entry.user_id = user_id
512 512 db_entry.position = position
513 513 db_entry.title = entry.get('title')
514 514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516 516
517 517 Session().add(db_entry)
518 518
519 519 @LoginRequired()
520 520 @NotAnonymous()
521 521 @CSRFRequired()
522 522 @view_config(
523 523 route_name='my_account_bookmarks_update', request_method='POST')
524 524 def my_account_bookmarks_update(self):
525 525 _ = self.request.translate
526 526 c = self.load_default_context()
527 527 c.active = 'bookmarks'
528 528
529 529 controls = peppercorn.parse(self.request.POST.items())
530 530 user_id = c.user.user_id
531 531
532 532 # validate positions
533 533 positions = {}
534 534 for entry in controls.get('bookmarks', []):
535 535 position = safe_int(entry['position'])
536 536 if position is None:
537 537 continue
538 538
539 539 if position in positions:
540 540 h.flash(_("Position {} is defined twice. "
541 541 "Please correct this error.").format(position), category='error')
542 542 return HTTPFound(h.route_path('my_account_bookmarks'))
543 543
544 544 entry['position'] = position
545 545 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 546 positions[position] = entry
547 547
548 548 try:
549 549 for entry in positions.values():
550 550 self._process_bookmark_entry(entry, user_id)
551 551
552 552 Session().commit()
553 553 h.flash(_("Update Bookmarks"), category='success')
554 554 except IntegrityError:
555 555 h.flash(_("Failed to update bookmarks. "
556 556 "Make sure an unique position is used."), category='error')
557 557
558 558 return HTTPFound(h.route_path('my_account_bookmarks'))
559 559
560 560 @LoginRequired()
561 561 @NotAnonymous()
562 562 @view_config(
563 563 route_name='my_account_goto_bookmark', request_method='GET',
564 564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 565 def my_account_goto_bookmark(self):
566 566
567 567 bookmark_id = self.request.matchdict['bookmark_id']
568 568 user_bookmark = UserBookmark().query()\
569 569 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 570 .filter(UserBookmark.position == bookmark_id).scalar()
571 571
572 572 redirect_url = h.route_path('my_account_bookmarks')
573 573 if not user_bookmark:
574 574 raise HTTPFound(redirect_url)
575 575
576 576 # repository set
577 577 if user_bookmark.repository:
578 578 repo_name = user_bookmark.repository.repo_name
579 579 base_redirect_url = h.route_path(
580 580 'repo_summary', repo_name=repo_name)
581 581 if user_bookmark.redirect_url and \
582 582 '${repo_url}' in user_bookmark.redirect_url:
583 583 redirect_url = string.Template(user_bookmark.redirect_url)\
584 584 .safe_substitute({'repo_url': base_redirect_url})
585 585 else:
586 586 redirect_url = base_redirect_url
587 587 # repository group set
588 588 elif user_bookmark.repository_group:
589 589 repo_group_name = user_bookmark.repository_group.group_name
590 590 base_redirect_url = h.route_path(
591 591 'repo_group_home', repo_group_name=repo_group_name)
592 592 if user_bookmark.redirect_url and \
593 593 '${repo_group_url}' in user_bookmark.redirect_url:
594 594 redirect_url = string.Template(user_bookmark.redirect_url)\
595 595 .safe_substitute({'repo_group_url': base_redirect_url})
596 596 else:
597 597 redirect_url = base_redirect_url
598 598 # custom URL set
599 599 elif user_bookmark.redirect_url:
600 600 server_url = h.route_url('home').rstrip('/')
601 601 redirect_url = string.Template(user_bookmark.redirect_url) \
602 602 .safe_substitute({'server_url': server_url})
603 603
604 604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 605 raise HTTPFound(redirect_url)
606 606
607 607 @LoginRequired()
608 608 @NotAnonymous()
609 609 @view_config(
610 610 route_name='my_account_perms', request_method='GET',
611 611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 612 def my_account_perms(self):
613 613 c = self.load_default_context()
614 614 c.active = 'perms'
615 615
616 616 c.perm_user = c.auth_user
617 617 return self._get_template_context(c)
618 618
619 619 @LoginRequired()
620 620 @NotAnonymous()
621 621 @view_config(
622 622 route_name='my_account_notifications', request_method='GET',
623 623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 624 def my_notifications(self):
625 625 c = self.load_default_context()
626 626 c.active = 'notifications'
627 627
628 628 return self._get_template_context(c)
629 629
630 630 @LoginRequired()
631 631 @NotAnonymous()
632 632 @CSRFRequired()
633 633 @view_config(
634 634 route_name='my_account_notifications_toggle_visibility',
635 635 request_method='POST', renderer='json_ext')
636 636 def my_notifications_toggle_visibility(self):
637 637 user = self._rhodecode_db_user
638 638 new_status = not user.user_data.get('notification_status', True)
639 639 user.update_userdata(notification_status=new_status)
640 640 Session().commit()
641 641 return user.user_data['notification_status']
642 642
643 643 @LoginRequired()
644 644 @NotAnonymous()
645 645 @view_config(
646 646 route_name='my_account_edit',
647 647 request_method='GET',
648 648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 649 def my_account_edit(self):
650 650 c = self.load_default_context()
651 651 c.active = 'profile_edit'
652 652 c.extern_type = c.user.extern_type
653 653 c.extern_name = c.user.extern_name
654 654
655 655 schema = user_schema.UserProfileSchema().bind(
656 656 username=c.user.username, user_emails=c.user.emails)
657 657 appstruct = {
658 658 'username': c.user.username,
659 659 'email': c.user.email,
660 660 'firstname': c.user.firstname,
661 661 'lastname': c.user.lastname,
662 662 'description': c.user.description,
663 663 }
664 664 c.form = forms.RcForm(
665 665 schema, appstruct=appstruct,
666 666 action=h.route_path('my_account_update'),
667 667 buttons=(forms.buttons.save, forms.buttons.reset))
668 668
669 669 return self._get_template_context(c)
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @CSRFRequired()
674 674 @view_config(
675 675 route_name='my_account_update',
676 676 request_method='POST',
677 677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 678 def my_account_update(self):
679 679 _ = self.request.translate
680 680 c = self.load_default_context()
681 681 c.active = 'profile_edit'
682 682 c.perm_user = c.auth_user
683 683 c.extern_type = c.user.extern_type
684 684 c.extern_name = c.user.extern_name
685 685
686 686 schema = user_schema.UserProfileSchema().bind(
687 687 username=c.user.username, user_emails=c.user.emails)
688 688 form = forms.RcForm(
689 689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690 690
691 691 controls = self.request.POST.items()
692 692 try:
693 693 valid_data = form.validate(controls)
694 694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 695 'new_password', 'password_confirmation']
696 696 if c.extern_type != "rhodecode":
697 697 # forbid updating username for external accounts
698 698 skip_attrs.append('username')
699 699 old_email = c.user.email
700 700 UserModel().update_user(
701 701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 702 **valid_data)
703 703 if old_email != valid_data['email']:
704 704 old = UserEmailMap.query() \
705 705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
706 706 old.email = old_email
707 707 h.flash(_('Your account was updated successfully'), category='success')
708 708 Session().commit()
709 709 except forms.ValidationFailure as e:
710 710 c.form = e
711 711 return self._get_template_context(c)
712 712 except Exception:
713 713 log.exception("Exception updating user")
714 714 h.flash(_('Error occurred during update of user'),
715 715 category='error')
716 716 raise HTTPFound(h.route_path('my_account_profile'))
717 717
718 718 def _get_pull_requests_list(self, statuses):
719 719 draw, start, limit = self._extract_chunk(self.request)
720 720 search_q, order_by, order_dir = self._extract_ordering(self.request)
721 721 _render = self.request.get_partial_renderer(
722 722 'rhodecode:templates/data_table/_dt_elements.mako')
723 723
724 724 pull_requests = PullRequestModel().get_im_participating_in(
725 725 user_id=self._rhodecode_user.user_id,
726 726 statuses=statuses, query=search_q,
727 727 offset=start, length=limit, order_by=order_by,
728 728 order_dir=order_dir)
729 729
730 730 pull_requests_total_count = PullRequestModel().count_im_participating_in(
731 731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732 732
733 733 data = []
734 734 comments_model = CommentsModel()
735 735 for pr in pull_requests:
736 736 repo_id = pr.target_repo_id
737 737 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, count_only=True)
738 repo_id, pull_request=pr, include_drafts=False, count_only=True)
739 739 owned = pr.user_id == self._rhodecode_user.user_id
740 740
741 741 data.append({
742 742 'target_repo': _render('pullrequest_target_repo',
743 743 pr.target_repo.repo_name),
744 744 'name': _render('pullrequest_name',
745 745 pr.pull_request_id, pr.pull_request_state,
746 746 pr.work_in_progress, pr.target_repo.repo_name,
747 747 short=True),
748 748 'name_raw': pr.pull_request_id,
749 749 'status': _render('pullrequest_status',
750 750 pr.calculated_review_status()),
751 751 'title': _render('pullrequest_title', pr.title, pr.description),
752 752 'description': h.escape(pr.description),
753 753 'updated_on': _render('pullrequest_updated_on',
754 754 h.datetime_to_time(pr.updated_on)),
755 755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 756 'created_on': _render('pullrequest_updated_on',
757 757 h.datetime_to_time(pr.created_on)),
758 758 'created_on_raw': h.datetime_to_time(pr.created_on),
759 759 'state': pr.pull_request_state,
760 760 'author': _render('pullrequest_author',
761 761 pr.author.full_contact, ),
762 762 'author_raw': pr.author.full_name,
763 763 'comments': _render('pullrequest_comments', comments_count),
764 764 'comments_raw': comments_count,
765 765 'closed': pr.is_closed(),
766 766 'owned': owned
767 767 })
768 768
769 769 # json used to render the grid
770 770 data = ({
771 771 'draw': draw,
772 772 'data': data,
773 773 'recordsTotal': pull_requests_total_count,
774 774 'recordsFiltered': pull_requests_total_count,
775 775 })
776 776 return data
777 777
778 778 @LoginRequired()
779 779 @NotAnonymous()
780 780 @view_config(
781 781 route_name='my_account_pullrequests',
782 782 request_method='GET',
783 783 renderer='rhodecode:templates/admin/my_account/my_account.mako')
784 784 def my_account_pullrequests(self):
785 785 c = self.load_default_context()
786 786 c.active = 'pullrequests'
787 787 req_get = self.request.GET
788 788
789 789 c.closed = str2bool(req_get.get('pr_show_closed'))
790 790
791 791 return self._get_template_context(c)
792 792
793 793 @LoginRequired()
794 794 @NotAnonymous()
795 795 @view_config(
796 796 route_name='my_account_pullrequests_data',
797 797 request_method='GET', renderer='json_ext')
798 798 def my_account_pullrequests_data(self):
799 799 self.load_default_context()
800 800 req_get = self.request.GET
801 801 closed = str2bool(req_get.get('closed'))
802 802
803 803 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
804 804 if closed:
805 805 statuses += [PullRequest.STATUS_CLOSED]
806 806
807 807 data = self._get_pull_requests_list(statuses=statuses)
808 808 return data
809 809
810 810 @LoginRequired()
811 811 @NotAnonymous()
812 812 @view_config(
813 813 route_name='my_account_user_group_membership',
814 814 request_method='GET',
815 815 renderer='rhodecode:templates/admin/my_account/my_account.mako')
816 816 def my_account_user_group_membership(self):
817 817 c = self.load_default_context()
818 818 c.active = 'user_group_membership'
819 819 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
820 820 for group in self._rhodecode_db_user.group_member]
821 821 c.user_groups = json.dumps(groups)
822 822 return self._get_template_context(c)
@@ -1,1856 +1,1857 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111 112
112 113 data.append({
113 114 'name': _render('pullrequest_name',
114 115 pr.pull_request_id, pr.pull_request_state,
115 116 pr.work_in_progress, pr.target_repo.repo_name,
116 117 short=True),
117 118 'name_raw': pr.pull_request_id,
118 119 'status': _render('pullrequest_status',
119 120 pr.calculated_review_status()),
120 121 'title': _render('pullrequest_title', pr.title, pr.description),
121 122 'description': h.escape(pr.description),
122 123 'updated_on': _render('pullrequest_updated_on',
123 124 h.datetime_to_time(pr.updated_on)),
124 125 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 126 'created_on': _render('pullrequest_updated_on',
126 127 h.datetime_to_time(pr.created_on)),
127 128 'created_on_raw': h.datetime_to_time(pr.created_on),
128 129 'state': pr.pull_request_state,
129 130 'author': _render('pullrequest_author',
130 131 pr.author.full_contact, ),
131 132 'author_raw': pr.author.full_name,
132 133 'comments': _render('pullrequest_comments', comments_count),
133 134 'comments_raw': comments_count,
134 135 'closed': pr.is_closed(),
135 136 })
136 137
137 138 data = ({
138 139 'draw': draw,
139 140 'data': data,
140 141 'recordsTotal': pull_requests_total_count,
141 142 'recordsFiltered': pull_requests_total_count,
142 143 })
143 144 return data
144 145
145 146 @LoginRequired()
146 147 @HasRepoPermissionAnyDecorator(
147 148 'repository.read', 'repository.write', 'repository.admin')
148 149 @view_config(
149 150 route_name='pullrequest_show_all', request_method='GET',
150 151 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 152 def pull_request_list(self):
152 153 c = self.load_default_context()
153 154
154 155 req_get = self.request.GET
155 156 c.source = str2bool(req_get.get('source'))
156 157 c.closed = str2bool(req_get.get('closed'))
157 158 c.my = str2bool(req_get.get('my'))
158 159 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 160 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160 161
161 162 c.active = 'open'
162 163 if c.my:
163 164 c.active = 'my'
164 165 if c.closed:
165 166 c.active = 'closed'
166 167 if c.awaiting_review and not c.source:
167 168 c.active = 'awaiting'
168 169 if c.source and not c.awaiting_review:
169 170 c.active = 'source'
170 171 if c.awaiting_my_review:
171 172 c.active = 'awaiting_my'
172 173
173 174 return self._get_template_context(c)
174 175
175 176 @LoginRequired()
176 177 @HasRepoPermissionAnyDecorator(
177 178 'repository.read', 'repository.write', 'repository.admin')
178 179 @view_config(
179 180 route_name='pullrequest_show_all_data', request_method='GET',
180 181 renderer='json_ext', xhr=True)
181 182 def pull_request_list_data(self):
182 183 self.load_default_context()
183 184
184 185 # additional filters
185 186 req_get = self.request.GET
186 187 source = str2bool(req_get.get('source'))
187 188 closed = str2bool(req_get.get('closed'))
188 189 my = str2bool(req_get.get('my'))
189 190 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 191 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191 192
192 193 filter_type = 'awaiting_review' if awaiting_review \
193 194 else 'awaiting_my_review' if awaiting_my_review \
194 195 else None
195 196
196 197 opened_by = None
197 198 if my:
198 199 opened_by = [self._rhodecode_user.user_id]
199 200
200 201 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 202 if closed:
202 203 statuses = [PullRequest.STATUS_CLOSED]
203 204
204 205 data = self._get_pull_requests_list(
205 206 repo_name=self.db_repo_name, source=source,
206 207 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207 208
208 209 return data
209 210
210 211 def _is_diff_cache_enabled(self, target_repo):
211 212 caching_enabled = self._get_general_setting(
212 213 target_repo, 'rhodecode_diff_cache')
213 214 log.debug('Diff caching enabled: %s', caching_enabled)
214 215 return caching_enabled
215 216
216 217 def _get_diffset(self, source_repo_name, source_repo,
217 218 ancestor_commit,
218 219 source_ref_id, target_ref_id,
219 220 target_commit, source_commit, diff_limit, file_limit,
220 221 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221 222
222 223 if use_ancestor:
223 224 # we might want to not use it for versions
224 225 target_ref_id = ancestor_commit.raw_id
225 226
226 227 vcs_diff = PullRequestModel().get_diff(
227 228 source_repo, source_ref_id, target_ref_id,
228 229 hide_whitespace_changes, diff_context)
229 230
230 231 diff_processor = diffs.DiffProcessor(
231 232 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 233 file_limit=file_limit, show_full_diff=fulldiff)
233 234
234 235 _parsed = diff_processor.prepare()
235 236
236 237 diffset = codeblocks.DiffSet(
237 238 repo_name=self.db_repo_name,
238 239 source_repo_name=source_repo_name,
239 240 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 241 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 242 )
242 243 diffset = self.path_filter.render_patchset_filtered(
243 244 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244 245
245 246 return diffset
246 247
247 248 def _get_range_diffset(self, source_scm, source_repo,
248 249 commit1, commit2, diff_limit, file_limit,
249 250 fulldiff, hide_whitespace_changes, diff_context):
250 251 vcs_diff = source_scm.get_diff(
251 252 commit1, commit2,
252 253 ignore_whitespace=hide_whitespace_changes,
253 254 context=diff_context)
254 255
255 256 diff_processor = diffs.DiffProcessor(
256 257 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 258 file_limit=file_limit, show_full_diff=fulldiff)
258 259
259 260 _parsed = diff_processor.prepare()
260 261
261 262 diffset = codeblocks.DiffSet(
262 263 repo_name=source_repo.repo_name,
263 264 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 265 target_node_getter=codeblocks.diffset_node_getter(commit2))
265 266
266 267 diffset = self.path_filter.render_patchset_filtered(
267 268 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268 269
269 270 return diffset
270 271
271 272 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 273 comments_model = CommentsModel()
273 274
274 275 # GENERAL COMMENTS with versions #
275 276 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 277 q = q.order_by(ChangesetComment.comment_id.asc())
277 278 if not include_drafts:
278 279 q = q.filter(ChangesetComment.draft == false())
279 280 general_comments = q
280 281
281 282 # pick comments we want to render at current version
282 283 c.comment_versions = comments_model.aggregate_comments(
283 284 general_comments, versions, c.at_version_num)
284 285
285 286 # INLINE COMMENTS with versions #
286 287 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 288 q = q.order_by(ChangesetComment.comment_id.asc())
288 289 if not include_drafts:
289 290 q = q.filter(ChangesetComment.draft == false())
290 291 inline_comments = q
291 292
292 293 c.inline_versions = comments_model.aggregate_comments(
293 294 inline_comments, versions, c.at_version_num, inline=True)
294 295
295 296 # Comments inline+general
296 297 if c.at_version:
297 298 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 299 c.comments = c.comment_versions[c.at_version_num]['display']
299 300 else:
300 301 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 302 c.comments = c.comment_versions[c.at_version_num]['until']
302 303
303 304 return general_comments, inline_comments
304 305
305 306 @LoginRequired()
306 307 @HasRepoPermissionAnyDecorator(
307 308 'repository.read', 'repository.write', 'repository.admin')
308 309 @view_config(
309 310 route_name='pullrequest_show', request_method='GET',
310 311 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
311 312 def pull_request_show(self):
312 313 _ = self.request.translate
313 314 c = self.load_default_context()
314 315
315 316 pull_request = PullRequest.get_or_404(
316 317 self.request.matchdict['pull_request_id'])
317 318 pull_request_id = pull_request.pull_request_id
318 319
319 320 c.state_progressing = pull_request.is_state_changing()
320 321 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321 322
322 323 _new_state = {
323 324 'created': PullRequest.STATE_CREATED,
324 325 }.get(self.request.GET.get('force_state'))
325 326
326 327 if c.is_super_admin and _new_state:
327 328 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
328 329 h.flash(
329 330 _('Pull Request state was force changed to `{}`').format(_new_state),
330 331 category='success')
331 332 Session().commit()
332 333
333 334 raise HTTPFound(h.route_path(
334 335 'pullrequest_show', repo_name=self.db_repo_name,
335 336 pull_request_id=pull_request_id))
336 337
337 338 version = self.request.GET.get('version')
338 339 from_version = self.request.GET.get('from_version') or version
339 340 merge_checks = self.request.GET.get('merge_checks')
340 341 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
341 342 force_refresh = str2bool(self.request.GET.get('force_refresh'))
342 343 c.range_diff_on = self.request.GET.get('range-diff') == "1"
343 344
344 345 # fetch global flags of ignore ws or context lines
345 346 diff_context = diffs.get_diff_context(self.request)
346 347 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
347 348
348 349 (pull_request_latest,
349 350 pull_request_at_ver,
350 351 pull_request_display_obj,
351 352 at_version) = PullRequestModel().get_pr_version(
352 353 pull_request_id, version=version)
353 354
354 355 pr_closed = pull_request_latest.is_closed()
355 356
356 357 if pr_closed and (version or from_version):
357 358 # not allow to browse versions for closed PR
358 359 raise HTTPFound(h.route_path(
359 360 'pullrequest_show', repo_name=self.db_repo_name,
360 361 pull_request_id=pull_request_id))
361 362
362 363 versions = pull_request_display_obj.versions()
363 364 # used to store per-commit range diffs
364 365 c.changes = collections.OrderedDict()
365 366
366 367 c.at_version = at_version
367 368 c.at_version_num = (at_version
368 369 if at_version and at_version != PullRequest.LATEST_VER
369 370 else None)
370 371
371 372 c.at_version_index = ChangesetComment.get_index_from_version(
372 373 c.at_version_num, versions)
373 374
374 375 (prev_pull_request_latest,
375 376 prev_pull_request_at_ver,
376 377 prev_pull_request_display_obj,
377 378 prev_at_version) = PullRequestModel().get_pr_version(
378 379 pull_request_id, version=from_version)
379 380
380 381 c.from_version = prev_at_version
381 382 c.from_version_num = (prev_at_version
382 383 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 384 else None)
384 385 c.from_version_index = ChangesetComment.get_index_from_version(
385 386 c.from_version_num, versions)
386 387
387 388 # define if we're in COMPARE mode or VIEW at version mode
388 389 compare = at_version != prev_at_version
389 390
390 391 # pull_requests repo_name we opened it against
391 392 # ie. target_repo must match
392 393 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 394 log.warning('Mismatch between the current repo: %s, and target %s',
394 395 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 396 raise HTTPNotFound()
396 397
397 398 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398 399
399 400 c.pull_request = pull_request_display_obj
400 401 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 402 c.pull_request_latest = pull_request_latest
402 403
403 404 # inject latest version
404 405 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 406 c.versions = versions + [latest_ver]
406 407
407 408 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 409 c.allowed_to_change_status = False
409 410 c.allowed_to_update = False
410 411 c.allowed_to_merge = False
411 412 c.allowed_to_delete = False
412 413 c.allowed_to_comment = False
413 414 c.allowed_to_close = False
414 415 else:
415 416 can_change_status = PullRequestModel().check_user_change_status(
416 417 pull_request_at_ver, self._rhodecode_user)
417 418 c.allowed_to_change_status = can_change_status and not pr_closed
418 419
419 420 c.allowed_to_update = PullRequestModel().check_user_update(
420 421 pull_request_latest, self._rhodecode_user) and not pr_closed
421 422 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 423 pull_request_latest, self._rhodecode_user) and not pr_closed
423 424 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 425 pull_request_latest, self._rhodecode_user) and not pr_closed
425 426 c.allowed_to_comment = not pr_closed
426 427 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427 428
428 429 c.forbid_adding_reviewers = False
429 430 c.forbid_author_to_review = False
430 431 c.forbid_commit_author_to_review = False
431 432
432 433 if pull_request_latest.reviewer_data and \
433 434 'rules' in pull_request_latest.reviewer_data:
434 435 rules = pull_request_latest.reviewer_data['rules'] or {}
435 436 try:
436 437 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 438 c.forbid_author_to_review = rules.get('forbid_author_to_review')
438 439 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
439 440 except Exception:
440 441 pass
441 442
442 443 # check merge capabilities
443 444 _merge_check = MergeCheck.validate(
444 445 pull_request_latest, auth_user=self._rhodecode_user,
445 446 translator=self.request.translate,
446 447 force_shadow_repo_refresh=force_refresh)
447 448
448 449 c.pr_merge_errors = _merge_check.error_details
449 450 c.pr_merge_possible = not _merge_check.failed
450 451 c.pr_merge_message = _merge_check.merge_msg
451 452 c.pr_merge_source_commit = _merge_check.source_commit
452 453 c.pr_merge_target_commit = _merge_check.target_commit
453 454
454 455 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 456 pull_request_latest, translator=self.request.translate)
456 457
457 458 c.pull_request_review_status = _merge_check.review_status
458 459 if merge_checks:
459 460 self.request.override_renderer = \
460 461 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 462 return self._get_template_context(c)
462 463
463 464 c.reviewers_count = pull_request.reviewers_count
464 465 c.observers_count = pull_request.observers_count
465 466
466 467 # reviewers and statuses
467 468 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
468 469 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 470 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470 471
471 472 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 473 member_reviewer = h.reviewer_as_json(
473 474 member, reasons=reasons, mandatory=mandatory,
474 475 role=review_obj.role,
475 476 user_group=review_obj.rule_user_group_data()
476 477 )
477 478
478 479 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 480 member_reviewer['review_status'] = current_review_status
480 481 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 482 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 483 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483 484
484 485 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
485 486
486 487 for observer_obj, member in pull_request_at_ver.observers():
487 488 member_observer = h.reviewer_as_json(
488 489 member, reasons=[], mandatory=False,
489 490 role=observer_obj.role,
490 491 user_group=observer_obj.rule_user_group_data()
491 492 )
492 493 member_observer['allowed_to_update'] = c.allowed_to_update
493 494 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494 495
495 496 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
496 497
497 498 general_comments, inline_comments = \
498 499 self.register_comments_vars(c, pull_request_latest, versions)
499 500
500 501 # TODOs
501 502 c.unresolved_comments = CommentsModel() \
502 503 .get_pull_request_unresolved_todos(pull_request_latest)
503 504 c.resolved_comments = CommentsModel() \
504 505 .get_pull_request_resolved_todos(pull_request_latest)
505 506
506 507 # if we use version, then do not show later comments
507 508 # than current version
508 509 display_inline_comments = collections.defaultdict(
509 510 lambda: collections.defaultdict(list))
510 511 for co in inline_comments:
511 512 if c.at_version_num:
512 513 # pick comments that are at least UPTO given version, so we
513 514 # don't render comments for higher version
514 515 should_render = co.pull_request_version_id and \
515 516 co.pull_request_version_id <= c.at_version_num
516 517 else:
517 518 # showing all, for 'latest'
518 519 should_render = True
519 520
520 521 if should_render:
521 522 display_inline_comments[co.f_path][co.line_no].append(co)
522 523
523 524 # load diff data into template context, if we use compare mode then
524 525 # diff is calculated based on changes between versions of PR
525 526
526 527 source_repo = pull_request_at_ver.source_repo
527 528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528 529
529 530 target_repo = pull_request_at_ver.target_repo
530 531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531 532
532 533 if compare:
533 534 # in compare switch the diff base to latest commit from prev version
534 535 target_ref_id = prev_pull_request_display_obj.revisions[0]
535 536
536 537 # despite opening commits for bookmarks/branches/tags, we always
537 538 # convert this to rev to prevent changes after bookmark or branch change
538 539 c.source_ref_type = 'rev'
539 540 c.source_ref = source_ref_id
540 541
541 542 c.target_ref_type = 'rev'
542 543 c.target_ref = target_ref_id
543 544
544 545 c.source_repo = source_repo
545 546 c.target_repo = target_repo
546 547
547 548 c.commit_ranges = []
548 549 source_commit = EmptyCommit()
549 550 target_commit = EmptyCommit()
550 551 c.missing_requirements = False
551 552
552 553 source_scm = source_repo.scm_instance()
553 554 target_scm = target_repo.scm_instance()
554 555
555 556 shadow_scm = None
556 557 try:
557 558 shadow_scm = pull_request_latest.get_shadow_repo()
558 559 except Exception:
559 560 log.debug('Failed to get shadow repo', exc_info=True)
560 561 # try first the existing source_repo, and then shadow
561 562 # repo if we can obtain one
562 563 commits_source_repo = source_scm
563 564 if shadow_scm:
564 565 commits_source_repo = shadow_scm
565 566
566 567 c.commits_source_repo = commits_source_repo
567 568 c.ancestor = None # set it to None, to hide it from PR view
568 569
569 570 # empty version means latest, so we keep this to prevent
570 571 # double caching
571 572 version_normalized = version or PullRequest.LATEST_VER
572 573 from_version_normalized = from_version or PullRequest.LATEST_VER
573 574
574 575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 576 cache_file_path = diff_cache_exist(
576 577 cache_path, 'pull_request', pull_request_id, version_normalized,
577 578 from_version_normalized, source_ref_id, target_ref_id,
578 579 hide_whitespace_changes, diff_context, c.fulldiff)
579 580
580 581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 582 force_recache = self.get_recache_flag()
582 583
583 584 cached_diff = None
584 585 if caching_enabled:
585 586 cached_diff = load_cached_diff(cache_file_path)
586 587
587 588 has_proper_commit_cache = (
588 589 cached_diff and cached_diff.get('commits')
589 590 and len(cached_diff.get('commits', [])) == 5
590 591 and cached_diff.get('commits')[0]
591 592 and cached_diff.get('commits')[3])
592 593
593 594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 595 diff_commit_cache = \
595 596 (ancestor_commit, commit_cache, missing_requirements,
596 597 source_commit, target_commit) = cached_diff['commits']
597 598 else:
598 599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 600 # merge errors resulting in potentially hidden commits in the shadow repo.
600 601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 602 and _merge_check.merge_response
602 603 maybe_unreachable = maybe_unreachable \
603 604 and _merge_check.merge_response.metadata.get('unresolved_files')
604 605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 606 diff_commit_cache = \
606 607 (ancestor_commit, commit_cache, missing_requirements,
607 608 source_commit, target_commit) = self.get_commits(
608 609 commits_source_repo,
609 610 pull_request_at_ver,
610 611 source_commit,
611 612 source_ref_id,
612 613 source_scm,
613 614 target_commit,
614 615 target_ref_id,
615 616 target_scm,
616 617 maybe_unreachable=maybe_unreachable)
617 618
618 619 # register our commit range
619 620 for comm in commit_cache.values():
620 621 c.commit_ranges.append(comm)
621 622
622 623 c.missing_requirements = missing_requirements
623 624 c.ancestor_commit = ancestor_commit
624 625 c.statuses = source_repo.statuses(
625 626 [x.raw_id for x in c.commit_ranges])
626 627
627 628 # auto collapse if we have more than limit
628 629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 631 c.compare_mode = compare
631 632
632 633 # diff_limit is the old behavior, will cut off the whole diff
633 634 # if the limit is applied otherwise will just hide the
634 635 # big files from the front-end
635 636 diff_limit = c.visual.cut_off_limit_diff
636 637 file_limit = c.visual.cut_off_limit_file
637 638
638 639 c.missing_commits = False
639 640 if (c.missing_requirements
640 641 or isinstance(source_commit, EmptyCommit)
641 642 or source_commit == target_commit):
642 643
643 644 c.missing_commits = True
644 645 else:
645 646 c.inline_comments = display_inline_comments
646 647
647 648 use_ancestor = True
648 649 if from_version_normalized != version_normalized:
649 650 use_ancestor = False
650 651
651 652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 653 if not force_recache and has_proper_diff_cache:
653 654 c.diffset = cached_diff['diff']
654 655 else:
655 656 try:
656 657 c.diffset = self._get_diffset(
657 658 c.source_repo.repo_name, commits_source_repo,
658 659 c.ancestor_commit,
659 660 source_ref_id, target_ref_id,
660 661 target_commit, source_commit,
661 662 diff_limit, file_limit, c.fulldiff,
662 663 hide_whitespace_changes, diff_context,
663 664 use_ancestor=use_ancestor
664 665 )
665 666
666 667 # save cached diff
667 668 if caching_enabled:
668 669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 670 except CommitDoesNotExistError:
670 671 log.exception('Failed to generate diffset')
671 672 c.missing_commits = True
672 673
673 674 if not c.missing_commits:
674 675
675 676 c.limited_diff = c.diffset.limited_diff
676 677
677 678 # calculate removed files that are bound to comments
678 679 comment_deleted_files = [
679 680 fname for fname in display_inline_comments
680 681 if fname not in c.diffset.file_stats]
681 682
682 683 c.deleted_files_comments = collections.defaultdict(dict)
683 684 for fname, per_line_comments in display_inline_comments.items():
684 685 if fname in comment_deleted_files:
685 686 c.deleted_files_comments[fname]['stats'] = 0
686 687 c.deleted_files_comments[fname]['comments'] = list()
687 688 for lno, comments in per_line_comments.items():
688 689 c.deleted_files_comments[fname]['comments'].extend(comments)
689 690
690 691 # maybe calculate the range diff
691 692 if c.range_diff_on:
692 693 # TODO(marcink): set whitespace/context
693 694 context_lcl = 3
694 695 ign_whitespace_lcl = False
695 696
696 697 for commit in c.commit_ranges:
697 698 commit2 = commit
698 699 commit1 = commit.first_parent
699 700
700 701 range_diff_cache_file_path = diff_cache_exist(
701 702 cache_path, 'diff', commit.raw_id,
702 703 ign_whitespace_lcl, context_lcl, c.fulldiff)
703 704
704 705 cached_diff = None
705 706 if caching_enabled:
706 707 cached_diff = load_cached_diff(range_diff_cache_file_path)
707 708
708 709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 710 if not force_recache and has_proper_diff_cache:
710 711 diffset = cached_diff['diff']
711 712 else:
712 713 diffset = self._get_range_diffset(
713 714 commits_source_repo, source_repo,
714 715 commit1, commit2, diff_limit, file_limit,
715 716 c.fulldiff, ign_whitespace_lcl, context_lcl
716 717 )
717 718
718 719 # save cached diff
719 720 if caching_enabled:
720 721 cache_diff(range_diff_cache_file_path, diffset, None)
721 722
722 723 c.changes[commit.raw_id] = diffset
723 724
724 725 # this is a hack to properly display links, when creating PR, the
725 726 # compare view and others uses different notation, and
726 727 # compare_commits.mako renders links based on the target_repo.
727 728 # We need to swap that here to generate it properly on the html side
728 729 c.target_repo = c.source_repo
729 730
730 731 c.commit_statuses = ChangesetStatus.STATUSES
731 732
732 733 c.show_version_changes = not pr_closed
733 734 if c.show_version_changes:
734 735 cur_obj = pull_request_at_ver
735 736 prev_obj = prev_pull_request_at_ver
736 737
737 738 old_commit_ids = prev_obj.revisions
738 739 new_commit_ids = cur_obj.revisions
739 740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 741 old_commit_ids, new_commit_ids)
741 742 c.commit_changes_summary = commit_changes
742 743
743 744 # calculate the diff for commits between versions
744 745 c.commit_changes = []
745 746
746 747 def mark(cs, fw):
747 748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748 749
749 750 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 751 + mark(commit_changes.removed, 'r') \
751 752 + mark(commit_changes.common, 'c'):
752 753
753 754 if raw_id in commit_cache:
754 755 commit = commit_cache[raw_id]
755 756 else:
756 757 try:
757 758 commit = commits_source_repo.get_commit(raw_id)
758 759 except CommitDoesNotExistError:
759 760 # in case we fail extracting still use "dummy" commit
760 761 # for display in commit diff
761 762 commit = h.AttributeDict(
762 763 {'raw_id': raw_id,
763 764 'message': 'EMPTY or MISSING COMMIT'})
764 765 c.commit_changes.append([c_type, commit])
765 766
766 767 # current user review statuses for each version
767 768 c.review_versions = {}
768 769 is_reviewer = PullRequestModel().is_user_reviewer(
769 770 pull_request, self._rhodecode_user)
770 771 if is_reviewer:
771 772 for co in general_comments:
772 773 if co.author.user_id == self._rhodecode_user.user_id:
773 774 status = co.status_change
774 775 if status:
775 776 _ver_pr = status[0].comment.pull_request_version_id
776 777 c.review_versions[_ver_pr] = status[0]
777 778
778 779 return self._get_template_context(c)
779 780
780 781 def get_commits(
781 782 self, commits_source_repo, pull_request_at_ver, source_commit,
782 783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 784 maybe_unreachable=False):
784 785
785 786 commit_cache = collections.OrderedDict()
786 787 missing_requirements = False
787 788
788 789 try:
789 790 pre_load = ["author", "date", "message", "branch", "parents"]
790 791
791 792 pull_request_commits = pull_request_at_ver.revisions
792 793 log.debug('Loading %s commits from %s',
793 794 len(pull_request_commits), commits_source_repo)
794 795
795 796 for rev in pull_request_commits:
796 797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 798 maybe_unreachable=maybe_unreachable)
798 799 commit_cache[comm.raw_id] = comm
799 800
800 801 # Order here matters, we first need to get target, and then
801 802 # the source
802 803 target_commit = commits_source_repo.get_commit(
803 804 commit_id=safe_str(target_ref_id))
804 805
805 806 source_commit = commits_source_repo.get_commit(
806 807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 808 except CommitDoesNotExistError:
808 809 log.warning('Failed to get commit from `{}` repo'.format(
809 810 commits_source_repo), exc_info=True)
810 811 except RepositoryRequirementError:
811 812 log.warning('Failed to get all required data from repo', exc_info=True)
812 813 missing_requirements = True
813 814
814 815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815 816
816 817 try:
817 818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 819 except Exception:
819 820 ancestor_commit = None
820 821
821 822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822 823
823 824 def assure_not_empty_repo(self):
824 825 _ = self.request.translate
825 826
826 827 try:
827 828 self.db_repo.scm_instance().get_commit()
828 829 except EmptyRepositoryError:
829 830 h.flash(h.literal(_('There are no commits yet')),
830 831 category='warning')
831 832 raise HTTPFound(
832 833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833 834
834 835 @LoginRequired()
835 836 @NotAnonymous()
836 837 @HasRepoPermissionAnyDecorator(
837 838 'repository.read', 'repository.write', 'repository.admin')
838 839 @view_config(
839 840 route_name='pullrequest_new', request_method='GET',
840 841 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
841 842 def pull_request_new(self):
842 843 _ = self.request.translate
843 844 c = self.load_default_context()
844 845
845 846 self.assure_not_empty_repo()
846 847 source_repo = self.db_repo
847 848
848 849 commit_id = self.request.GET.get('commit')
849 850 branch_ref = self.request.GET.get('branch')
850 851 bookmark_ref = self.request.GET.get('bookmark')
851 852
852 853 try:
853 854 source_repo_data = PullRequestModel().generate_repo_data(
854 855 source_repo, commit_id=commit_id,
855 856 branch=branch_ref, bookmark=bookmark_ref,
856 857 translator=self.request.translate)
857 858 except CommitDoesNotExistError as e:
858 859 log.exception(e)
859 860 h.flash(_('Commit does not exist'), 'error')
860 861 raise HTTPFound(
861 862 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
862 863
863 864 default_target_repo = source_repo
864 865
865 866 if source_repo.parent and c.has_origin_repo_read_perm:
866 867 parent_vcs_obj = source_repo.parent.scm_instance()
867 868 if parent_vcs_obj and not parent_vcs_obj.is_empty():
868 869 # change default if we have a parent repo
869 870 default_target_repo = source_repo.parent
870 871
871 872 target_repo_data = PullRequestModel().generate_repo_data(
872 873 default_target_repo, translator=self.request.translate)
873 874
874 875 selected_source_ref = source_repo_data['refs']['selected_ref']
875 876 title_source_ref = ''
876 877 if selected_source_ref:
877 878 title_source_ref = selected_source_ref.split(':', 2)[1]
878 879 c.default_title = PullRequestModel().generate_pullrequest_title(
879 880 source=source_repo.repo_name,
880 881 source_ref=title_source_ref,
881 882 target=default_target_repo.repo_name
882 883 )
883 884
884 885 c.default_repo_data = {
885 886 'source_repo_name': source_repo.repo_name,
886 887 'source_refs_json': json.dumps(source_repo_data),
887 888 'target_repo_name': default_target_repo.repo_name,
888 889 'target_refs_json': json.dumps(target_repo_data),
889 890 }
890 891 c.default_source_ref = selected_source_ref
891 892
892 893 return self._get_template_context(c)
893 894
894 895 @LoginRequired()
895 896 @NotAnonymous()
896 897 @HasRepoPermissionAnyDecorator(
897 898 'repository.read', 'repository.write', 'repository.admin')
898 899 @view_config(
899 900 route_name='pullrequest_repo_refs', request_method='GET',
900 901 renderer='json_ext', xhr=True)
901 902 def pull_request_repo_refs(self):
902 903 self.load_default_context()
903 904 target_repo_name = self.request.matchdict['target_repo_name']
904 905 repo = Repository.get_by_repo_name(target_repo_name)
905 906 if not repo:
906 907 raise HTTPNotFound()
907 908
908 909 target_perm = HasRepoPermissionAny(
909 910 'repository.read', 'repository.write', 'repository.admin')(
910 911 target_repo_name)
911 912 if not target_perm:
912 913 raise HTTPNotFound()
913 914
914 915 return PullRequestModel().generate_repo_data(
915 916 repo, translator=self.request.translate)
916 917
917 918 @LoginRequired()
918 919 @NotAnonymous()
919 920 @HasRepoPermissionAnyDecorator(
920 921 'repository.read', 'repository.write', 'repository.admin')
921 922 @view_config(
922 923 route_name='pullrequest_repo_targets', request_method='GET',
923 924 renderer='json_ext', xhr=True)
924 925 def pullrequest_repo_targets(self):
925 926 _ = self.request.translate
926 927 filter_query = self.request.GET.get('query')
927 928
928 929 # get the parents
929 930 parent_target_repos = []
930 931 if self.db_repo.parent:
931 932 parents_query = Repository.query() \
932 933 .order_by(func.length(Repository.repo_name)) \
933 934 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 935
935 936 if filter_query:
936 937 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 938 parents_query = parents_query.filter(
938 939 Repository.repo_name.ilike(ilike_expression))
939 940 parents = parents_query.limit(20).all()
940 941
941 942 for parent in parents:
942 943 parent_vcs_obj = parent.scm_instance()
943 944 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 945 parent_target_repos.append(parent)
945 946
946 947 # get other forks, and repo itself
947 948 query = Repository.query() \
948 949 .order_by(func.length(Repository.repo_name)) \
949 950 .filter(
950 951 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 952 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 953 ) \
953 954 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 955
955 956 if filter_query:
956 957 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 958 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 959
959 960 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 961 target_repos = query.limit(limit).all()
961 962
962 963 all_target_repos = target_repos + parent_target_repos
963 964
964 965 repos = []
965 966 # This checks permissions to the repositories
966 967 for obj in ScmModel().get_repos(all_target_repos):
967 968 repos.append({
968 969 'id': obj['name'],
969 970 'text': obj['name'],
970 971 'type': 'repo',
971 972 'repo_id': obj['dbrepo']['repo_id'],
972 973 'repo_type': obj['dbrepo']['repo_type'],
973 974 'private': obj['dbrepo']['private'],
974 975
975 976 })
976 977
977 978 data = {
978 979 'more': False,
979 980 'results': [{
980 981 'text': _('Repositories'),
981 982 'children': repos
982 983 }] if repos else []
983 984 }
984 985 return data
985 986
986 987 @classmethod
987 988 def get_comment_ids(cls, post_data):
988 989 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989 990
990 991 @LoginRequired()
991 992 @NotAnonymous()
992 993 @HasRepoPermissionAnyDecorator(
993 994 'repository.read', 'repository.write', 'repository.admin')
994 995 @view_config(
995 996 route_name='pullrequest_comments', request_method='POST',
996 997 renderer='string_html', xhr=True)
997 998 def pullrequest_comments(self):
998 999 self.load_default_context()
999 1000
1000 1001 pull_request = PullRequest.get_or_404(
1001 1002 self.request.matchdict['pull_request_id'])
1002 1003 pull_request_id = pull_request.pull_request_id
1003 1004 version = self.request.GET.get('version')
1004 1005
1005 1006 _render = self.request.get_partial_renderer(
1006 1007 'rhodecode:templates/base/sidebar.mako')
1007 1008 c = _render.get_call_context()
1008 1009
1009 1010 (pull_request_latest,
1010 1011 pull_request_at_ver,
1011 1012 pull_request_display_obj,
1012 1013 at_version) = PullRequestModel().get_pr_version(
1013 1014 pull_request_id, version=version)
1014 1015 versions = pull_request_display_obj.versions()
1015 1016 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1016 1017 c.versions = versions + [latest_ver]
1017 1018
1018 1019 c.at_version = at_version
1019 1020 c.at_version_num = (at_version
1020 1021 if at_version and at_version != PullRequest.LATEST_VER
1021 1022 else None)
1022 1023
1023 1024 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1024 1025 all_comments = c.inline_comments_flat + c.comments
1025 1026
1026 1027 existing_ids = self.get_comment_ids(self.request.POST)
1027 1028 return _render('comments_table', all_comments, len(all_comments),
1028 1029 existing_ids=existing_ids)
1029 1030
1030 1031 @LoginRequired()
1031 1032 @NotAnonymous()
1032 1033 @HasRepoPermissionAnyDecorator(
1033 1034 'repository.read', 'repository.write', 'repository.admin')
1034 1035 @view_config(
1035 1036 route_name='pullrequest_todos', request_method='POST',
1036 1037 renderer='string_html', xhr=True)
1037 1038 def pullrequest_todos(self):
1038 1039 self.load_default_context()
1039 1040
1040 1041 pull_request = PullRequest.get_or_404(
1041 1042 self.request.matchdict['pull_request_id'])
1042 1043 pull_request_id = pull_request.pull_request_id
1043 1044 version = self.request.GET.get('version')
1044 1045
1045 1046 _render = self.request.get_partial_renderer(
1046 1047 'rhodecode:templates/base/sidebar.mako')
1047 1048 c = _render.get_call_context()
1048 1049 (pull_request_latest,
1049 1050 pull_request_at_ver,
1050 1051 pull_request_display_obj,
1051 1052 at_version) = PullRequestModel().get_pr_version(
1052 1053 pull_request_id, version=version)
1053 1054 versions = pull_request_display_obj.versions()
1054 1055 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1055 1056 c.versions = versions + [latest_ver]
1056 1057
1057 1058 c.at_version = at_version
1058 1059 c.at_version_num = (at_version
1059 1060 if at_version and at_version != PullRequest.LATEST_VER
1060 1061 else None)
1061 1062
1062 1063 c.unresolved_comments = CommentsModel() \
1063 1064 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1064 1065 c.resolved_comments = CommentsModel() \
1065 1066 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1066 1067
1067 1068 all_comments = c.unresolved_comments + c.resolved_comments
1068 1069 existing_ids = self.get_comment_ids(self.request.POST)
1069 1070 return _render('comments_table', all_comments, len(c.unresolved_comments),
1070 1071 todo_comments=True, existing_ids=existing_ids)
1071 1072
1072 1073 @LoginRequired()
1073 1074 @NotAnonymous()
1074 1075 @HasRepoPermissionAnyDecorator(
1075 1076 'repository.read', 'repository.write', 'repository.admin')
1076 1077 @CSRFRequired()
1077 1078 @view_config(
1078 1079 route_name='pullrequest_create', request_method='POST',
1079 1080 renderer=None)
1080 1081 def pull_request_create(self):
1081 1082 _ = self.request.translate
1082 1083 self.assure_not_empty_repo()
1083 1084 self.load_default_context()
1084 1085
1085 1086 controls = peppercorn.parse(self.request.POST.items())
1086 1087
1087 1088 try:
1088 1089 form = PullRequestForm(
1089 1090 self.request.translate, self.db_repo.repo_id)()
1090 1091 _form = form.to_python(controls)
1091 1092 except formencode.Invalid as errors:
1092 1093 if errors.error_dict.get('revisions'):
1093 1094 msg = 'Revisions: %s' % errors.error_dict['revisions']
1094 1095 elif errors.error_dict.get('pullrequest_title'):
1095 1096 msg = errors.error_dict.get('pullrequest_title')
1096 1097 else:
1097 1098 msg = _('Error creating pull request: {}').format(errors)
1098 1099 log.exception(msg)
1099 1100 h.flash(msg, 'error')
1100 1101
1101 1102 # would rather just go back to form ...
1102 1103 raise HTTPFound(
1103 1104 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1104 1105
1105 1106 source_repo = _form['source_repo']
1106 1107 source_ref = _form['source_ref']
1107 1108 target_repo = _form['target_repo']
1108 1109 target_ref = _form['target_ref']
1109 1110 commit_ids = _form['revisions'][::-1]
1110 1111 common_ancestor_id = _form['common_ancestor']
1111 1112
1112 1113 # find the ancestor for this pr
1113 1114 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1114 1115 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1115 1116
1116 1117 if not (source_db_repo or target_db_repo):
1117 1118 h.flash(_('source_repo or target repo not found'), category='error')
1118 1119 raise HTTPFound(
1119 1120 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1120 1121
1121 1122 # re-check permissions again here
1122 1123 # source_repo we must have read permissions
1123 1124
1124 1125 source_perm = HasRepoPermissionAny(
1125 1126 'repository.read', 'repository.write', 'repository.admin')(
1126 1127 source_db_repo.repo_name)
1127 1128 if not source_perm:
1128 1129 msg = _('Not Enough permissions to source repo `{}`.'.format(
1129 1130 source_db_repo.repo_name))
1130 1131 h.flash(msg, category='error')
1131 1132 # copy the args back to redirect
1132 1133 org_query = self.request.GET.mixed()
1133 1134 raise HTTPFound(
1134 1135 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1135 1136 _query=org_query))
1136 1137
1137 1138 # target repo we must have read permissions, and also later on
1138 1139 # we want to check branch permissions here
1139 1140 target_perm = HasRepoPermissionAny(
1140 1141 'repository.read', 'repository.write', 'repository.admin')(
1141 1142 target_db_repo.repo_name)
1142 1143 if not target_perm:
1143 1144 msg = _('Not Enough permissions to target repo `{}`.'.format(
1144 1145 target_db_repo.repo_name))
1145 1146 h.flash(msg, category='error')
1146 1147 # copy the args back to redirect
1147 1148 org_query = self.request.GET.mixed()
1148 1149 raise HTTPFound(
1149 1150 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1150 1151 _query=org_query))
1151 1152
1152 1153 source_scm = source_db_repo.scm_instance()
1153 1154 target_scm = target_db_repo.scm_instance()
1154 1155
1155 1156 source_ref_obj = unicode_to_reference(source_ref)
1156 1157 target_ref_obj = unicode_to_reference(target_ref)
1157 1158
1158 1159 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1159 1160 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1160 1161
1161 1162 ancestor = source_scm.get_common_ancestor(
1162 1163 source_commit.raw_id, target_commit.raw_id, target_scm)
1163 1164
1164 1165 # recalculate target ref based on ancestor
1165 1166 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1166 1167
1167 1168 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1168 1169 PullRequestModel().get_reviewer_functions()
1169 1170
1170 1171 # recalculate reviewers logic, to make sure we can validate this
1171 1172 reviewer_rules = get_default_reviewers_data(
1172 1173 self._rhodecode_db_user,
1173 1174 source_db_repo,
1174 1175 source_ref_obj,
1175 1176 target_db_repo,
1176 1177 target_ref_obj,
1177 1178 include_diff_info=False)
1178 1179
1179 1180 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1180 1181 observers = validate_observers(_form['observer_members'], reviewer_rules)
1181 1182
1182 1183 pullrequest_title = _form['pullrequest_title']
1183 1184 title_source_ref = source_ref_obj.name
1184 1185 if not pullrequest_title:
1185 1186 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1186 1187 source=source_repo,
1187 1188 source_ref=title_source_ref,
1188 1189 target=target_repo
1189 1190 )
1190 1191
1191 1192 description = _form['pullrequest_desc']
1192 1193 description_renderer = _form['description_renderer']
1193 1194
1194 1195 try:
1195 1196 pull_request = PullRequestModel().create(
1196 1197 created_by=self._rhodecode_user.user_id,
1197 1198 source_repo=source_repo,
1198 1199 source_ref=source_ref,
1199 1200 target_repo=target_repo,
1200 1201 target_ref=target_ref,
1201 1202 revisions=commit_ids,
1202 1203 common_ancestor_id=common_ancestor_id,
1203 1204 reviewers=reviewers,
1204 1205 observers=observers,
1205 1206 title=pullrequest_title,
1206 1207 description=description,
1207 1208 description_renderer=description_renderer,
1208 1209 reviewer_data=reviewer_rules,
1209 1210 auth_user=self._rhodecode_user
1210 1211 )
1211 1212 Session().commit()
1212 1213
1213 1214 h.flash(_('Successfully opened new pull request'),
1214 1215 category='success')
1215 1216 except Exception:
1216 1217 msg = _('Error occurred during creation of this pull request.')
1217 1218 log.exception(msg)
1218 1219 h.flash(msg, category='error')
1219 1220
1220 1221 # copy the args back to redirect
1221 1222 org_query = self.request.GET.mixed()
1222 1223 raise HTTPFound(
1223 1224 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1224 1225 _query=org_query))
1225 1226
1226 1227 raise HTTPFound(
1227 1228 h.route_path('pullrequest_show', repo_name=target_repo,
1228 1229 pull_request_id=pull_request.pull_request_id))
1229 1230
1230 1231 @LoginRequired()
1231 1232 @NotAnonymous()
1232 1233 @HasRepoPermissionAnyDecorator(
1233 1234 'repository.read', 'repository.write', 'repository.admin')
1234 1235 @CSRFRequired()
1235 1236 @view_config(
1236 1237 route_name='pullrequest_update', request_method='POST',
1237 1238 renderer='json_ext')
1238 1239 def pull_request_update(self):
1239 1240 pull_request = PullRequest.get_or_404(
1240 1241 self.request.matchdict['pull_request_id'])
1241 1242 _ = self.request.translate
1242 1243
1243 1244 c = self.load_default_context()
1244 1245 redirect_url = None
1245 1246
1246 1247 if pull_request.is_closed():
1247 1248 log.debug('update: forbidden because pull request is closed')
1248 1249 msg = _(u'Cannot update closed pull requests.')
1249 1250 h.flash(msg, category='error')
1250 1251 return {'response': True,
1251 1252 'redirect_url': redirect_url}
1252 1253
1253 1254 is_state_changing = pull_request.is_state_changing()
1254 1255 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1255 1256
1256 1257 # only owner or admin can update it
1257 1258 allowed_to_update = PullRequestModel().check_user_update(
1258 1259 pull_request, self._rhodecode_user)
1259 1260
1260 1261 if allowed_to_update:
1261 1262 controls = peppercorn.parse(self.request.POST.items())
1262 1263 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1263 1264
1264 1265 if 'review_members' in controls:
1265 1266 self._update_reviewers(
1266 1267 c,
1267 1268 pull_request, controls['review_members'],
1268 1269 pull_request.reviewer_data,
1269 1270 PullRequestReviewers.ROLE_REVIEWER)
1270 1271 elif 'observer_members' in controls:
1271 1272 self._update_reviewers(
1272 1273 c,
1273 1274 pull_request, controls['observer_members'],
1274 1275 pull_request.reviewer_data,
1275 1276 PullRequestReviewers.ROLE_OBSERVER)
1276 1277 elif str2bool(self.request.POST.get('update_commits', 'false')):
1277 1278 if is_state_changing:
1278 1279 log.debug('commits update: forbidden because pull request is in state %s',
1279 1280 pull_request.pull_request_state)
1280 1281 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1281 1282 u'Current state is: `{}`').format(
1282 1283 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1283 1284 h.flash(msg, category='error')
1284 1285 return {'response': True,
1285 1286 'redirect_url': redirect_url}
1286 1287
1287 1288 self._update_commits(c, pull_request)
1288 1289 if force_refresh:
1289 1290 redirect_url = h.route_path(
1290 1291 'pullrequest_show', repo_name=self.db_repo_name,
1291 1292 pull_request_id=pull_request.pull_request_id,
1292 1293 _query={"force_refresh": 1})
1293 1294 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1294 1295 self._edit_pull_request(pull_request)
1295 1296 else:
1296 1297 log.error('Unhandled update data.')
1297 1298 raise HTTPBadRequest()
1298 1299
1299 1300 return {'response': True,
1300 1301 'redirect_url': redirect_url}
1301 1302 raise HTTPForbidden()
1302 1303
1303 1304 def _edit_pull_request(self, pull_request):
1304 1305 """
1305 1306 Edit title and description
1306 1307 """
1307 1308 _ = self.request.translate
1308 1309
1309 1310 try:
1310 1311 PullRequestModel().edit(
1311 1312 pull_request,
1312 1313 self.request.POST.get('title'),
1313 1314 self.request.POST.get('description'),
1314 1315 self.request.POST.get('description_renderer'),
1315 1316 self._rhodecode_user)
1316 1317 except ValueError:
1317 1318 msg = _(u'Cannot update closed pull requests.')
1318 1319 h.flash(msg, category='error')
1319 1320 return
1320 1321 else:
1321 1322 Session().commit()
1322 1323
1323 1324 msg = _(u'Pull request title & description updated.')
1324 1325 h.flash(msg, category='success')
1325 1326 return
1326 1327
1327 1328 def _update_commits(self, c, pull_request):
1328 1329 _ = self.request.translate
1329 1330
1330 1331 with pull_request.set_state(PullRequest.STATE_UPDATING):
1331 1332 resp = PullRequestModel().update_commits(
1332 1333 pull_request, self._rhodecode_db_user)
1333 1334
1334 1335 if resp.executed:
1335 1336
1336 1337 if resp.target_changed and resp.source_changed:
1337 1338 changed = 'target and source repositories'
1338 1339 elif resp.target_changed and not resp.source_changed:
1339 1340 changed = 'target repository'
1340 1341 elif not resp.target_changed and resp.source_changed:
1341 1342 changed = 'source repository'
1342 1343 else:
1343 1344 changed = 'nothing'
1344 1345
1345 1346 msg = _(u'Pull request updated to "{source_commit_id}" with '
1346 1347 u'{count_added} added, {count_removed} removed commits. '
1347 1348 u'Source of changes: {change_source}.')
1348 1349 msg = msg.format(
1349 1350 source_commit_id=pull_request.source_ref_parts.commit_id,
1350 1351 count_added=len(resp.changes.added),
1351 1352 count_removed=len(resp.changes.removed),
1352 1353 change_source=changed)
1353 1354 h.flash(msg, category='success')
1354 1355 channelstream.pr_update_channelstream_push(
1355 1356 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1356 1357 else:
1357 1358 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1358 1359 warning_reasons = [
1359 1360 UpdateFailureReason.NO_CHANGE,
1360 1361 UpdateFailureReason.WRONG_REF_TYPE,
1361 1362 ]
1362 1363 category = 'warning' if resp.reason in warning_reasons else 'error'
1363 1364 h.flash(msg, category=category)
1364 1365
1365 1366 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1366 1367 _ = self.request.translate
1367 1368
1368 1369 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1369 1370 PullRequestModel().get_reviewer_functions()
1370 1371
1371 1372 if role == PullRequestReviewers.ROLE_REVIEWER:
1372 1373 try:
1373 1374 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1374 1375 except ValueError as e:
1375 1376 log.error('Reviewers Validation: {}'.format(e))
1376 1377 h.flash(e, category='error')
1377 1378 return
1378 1379
1379 1380 old_calculated_status = pull_request.calculated_review_status()
1380 1381 PullRequestModel().update_reviewers(
1381 1382 pull_request, reviewers, self._rhodecode_db_user)
1382 1383
1383 1384 Session().commit()
1384 1385
1385 1386 msg = _('Pull request reviewers updated.')
1386 1387 h.flash(msg, category='success')
1387 1388 channelstream.pr_update_channelstream_push(
1388 1389 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1389 1390
1390 1391 # trigger status changed if change in reviewers changes the status
1391 1392 calculated_status = pull_request.calculated_review_status()
1392 1393 if old_calculated_status != calculated_status:
1393 1394 PullRequestModel().trigger_pull_request_hook(
1394 1395 pull_request, self._rhodecode_user, 'review_status_change',
1395 1396 data={'status': calculated_status})
1396 1397
1397 1398 elif role == PullRequestReviewers.ROLE_OBSERVER:
1398 1399 try:
1399 1400 observers = validate_observers(review_members, reviewer_rules)
1400 1401 except ValueError as e:
1401 1402 log.error('Observers Validation: {}'.format(e))
1402 1403 h.flash(e, category='error')
1403 1404 return
1404 1405
1405 1406 PullRequestModel().update_observers(
1406 1407 pull_request, observers, self._rhodecode_db_user)
1407 1408
1408 1409 Session().commit()
1409 1410 msg = _('Pull request observers updated.')
1410 1411 h.flash(msg, category='success')
1411 1412 channelstream.pr_update_channelstream_push(
1412 1413 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1413 1414
1414 1415 @LoginRequired()
1415 1416 @NotAnonymous()
1416 1417 @HasRepoPermissionAnyDecorator(
1417 1418 'repository.read', 'repository.write', 'repository.admin')
1418 1419 @CSRFRequired()
1419 1420 @view_config(
1420 1421 route_name='pullrequest_merge', request_method='POST',
1421 1422 renderer='json_ext')
1422 1423 def pull_request_merge(self):
1423 1424 """
1424 1425 Merge will perform a server-side merge of the specified
1425 1426 pull request, if the pull request is approved and mergeable.
1426 1427 After successful merging, the pull request is automatically
1427 1428 closed, with a relevant comment.
1428 1429 """
1429 1430 pull_request = PullRequest.get_or_404(
1430 1431 self.request.matchdict['pull_request_id'])
1431 1432 _ = self.request.translate
1432 1433
1433 1434 if pull_request.is_state_changing():
1434 1435 log.debug('show: forbidden because pull request is in state %s',
1435 1436 pull_request.pull_request_state)
1436 1437 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1437 1438 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1438 1439 pull_request.pull_request_state)
1439 1440 h.flash(msg, category='error')
1440 1441 raise HTTPFound(
1441 1442 h.route_path('pullrequest_show',
1442 1443 repo_name=pull_request.target_repo.repo_name,
1443 1444 pull_request_id=pull_request.pull_request_id))
1444 1445
1445 1446 self.load_default_context()
1446 1447
1447 1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 1449 check = MergeCheck.validate(
1449 1450 pull_request, auth_user=self._rhodecode_user,
1450 1451 translator=self.request.translate)
1451 1452 merge_possible = not check.failed
1452 1453
1453 1454 for err_type, error_msg in check.errors:
1454 1455 h.flash(error_msg, category=err_type)
1455 1456
1456 1457 if merge_possible:
1457 1458 log.debug("Pre-conditions checked, trying to merge.")
1458 1459 extras = vcs_operation_context(
1459 1460 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1460 1461 username=self._rhodecode_db_user.username, action='push',
1461 1462 scm=pull_request.target_repo.repo_type)
1462 1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1463 1464 self._merge_pull_request(
1464 1465 pull_request, self._rhodecode_db_user, extras)
1465 1466 else:
1466 1467 log.debug("Pre-conditions failed, NOT merging.")
1467 1468
1468 1469 raise HTTPFound(
1469 1470 h.route_path('pullrequest_show',
1470 1471 repo_name=pull_request.target_repo.repo_name,
1471 1472 pull_request_id=pull_request.pull_request_id))
1472 1473
1473 1474 def _merge_pull_request(self, pull_request, user, extras):
1474 1475 _ = self.request.translate
1475 1476 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1476 1477
1477 1478 if merge_resp.executed:
1478 1479 log.debug("The merge was successful, closing the pull request.")
1479 1480 PullRequestModel().close_pull_request(
1480 1481 pull_request.pull_request_id, user)
1481 1482 Session().commit()
1482 1483 msg = _('Pull request was successfully merged and closed.')
1483 1484 h.flash(msg, category='success')
1484 1485 else:
1485 1486 log.debug(
1486 1487 "The merge was not successful. Merge response: %s", merge_resp)
1487 1488 msg = merge_resp.merge_status_message
1488 1489 h.flash(msg, category='error')
1489 1490
1490 1491 @LoginRequired()
1491 1492 @NotAnonymous()
1492 1493 @HasRepoPermissionAnyDecorator(
1493 1494 'repository.read', 'repository.write', 'repository.admin')
1494 1495 @CSRFRequired()
1495 1496 @view_config(
1496 1497 route_name='pullrequest_delete', request_method='POST',
1497 1498 renderer='json_ext')
1498 1499 def pull_request_delete(self):
1499 1500 _ = self.request.translate
1500 1501
1501 1502 pull_request = PullRequest.get_or_404(
1502 1503 self.request.matchdict['pull_request_id'])
1503 1504 self.load_default_context()
1504 1505
1505 1506 pr_closed = pull_request.is_closed()
1506 1507 allowed_to_delete = PullRequestModel().check_user_delete(
1507 1508 pull_request, self._rhodecode_user) and not pr_closed
1508 1509
1509 1510 # only owner can delete it !
1510 1511 if allowed_to_delete:
1511 1512 PullRequestModel().delete(pull_request, self._rhodecode_user)
1512 1513 Session().commit()
1513 1514 h.flash(_('Successfully deleted pull request'),
1514 1515 category='success')
1515 1516 raise HTTPFound(h.route_path('pullrequest_show_all',
1516 1517 repo_name=self.db_repo_name))
1517 1518
1518 1519 log.warning('user %s tried to delete pull request without access',
1519 1520 self._rhodecode_user)
1520 1521 raise HTTPNotFound()
1521 1522
1522 1523 def _pull_request_comments_create(self, pull_request, comments):
1523 1524 _ = self.request.translate
1524 1525 data = {}
1525 1526 pull_request_id = pull_request.pull_request_id
1526 1527 if not comments:
1527 1528 return
1528 1529
1529 1530 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1530 1531
1531 1532 for entry in comments:
1532 1533 c = self.load_default_context()
1533 1534 comment_type = entry['comment_type']
1534 1535 text = entry['text']
1535 1536 status = entry['status']
1536 1537 is_draft = str2bool(entry['is_draft'])
1537 1538 resolves_comment_id = entry['resolves_comment_id']
1538 1539 close_pull_request = entry['close_pull_request']
1539 1540 f_path = entry['f_path']
1540 1541 line_no = entry['line']
1541 1542 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1542 1543
1543 1544 # the logic here should work like following, if we submit close
1544 1545 # pr comment, use `close_pull_request_with_comment` function
1545 1546 # else handle regular comment logic
1546 1547
1547 1548 if close_pull_request:
1548 1549 # only owner or admin or person with write permissions
1549 1550 allowed_to_close = PullRequestModel().check_user_update(
1550 1551 pull_request, self._rhodecode_user)
1551 1552 if not allowed_to_close:
1552 1553 log.debug('comment: forbidden because not allowed to close '
1553 1554 'pull request %s', pull_request_id)
1554 1555 raise HTTPForbidden()
1555 1556
1556 1557 # This also triggers `review_status_change`
1557 1558 comment, status = PullRequestModel().close_pull_request_with_comment(
1558 1559 pull_request, self._rhodecode_user, self.db_repo, message=text,
1559 1560 auth_user=self._rhodecode_user)
1560 1561 Session().flush()
1561 1562 is_inline = comment.is_inline
1562 1563
1563 1564 PullRequestModel().trigger_pull_request_hook(
1564 1565 pull_request, self._rhodecode_user, 'comment',
1565 1566 data={'comment': comment})
1566 1567
1567 1568 else:
1568 1569 # regular comment case, could be inline, or one with status.
1569 1570 # for that one we check also permissions
1570 1571 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1571 1572 allowed_to_change_status = PullRequestModel().check_user_change_status(
1572 1573 pull_request, self._rhodecode_user) and not is_draft
1573 1574
1574 1575 if status and allowed_to_change_status:
1575 1576 message = (_('Status change %(transition_icon)s %(status)s')
1576 1577 % {'transition_icon': '>',
1577 1578 'status': ChangesetStatus.get_status_lbl(status)})
1578 1579 text = text or message
1579 1580
1580 1581 comment = CommentsModel().create(
1581 1582 text=text,
1582 1583 repo=self.db_repo.repo_id,
1583 1584 user=self._rhodecode_user.user_id,
1584 1585 pull_request=pull_request,
1585 1586 f_path=f_path,
1586 1587 line_no=line_no,
1587 1588 status_change=(ChangesetStatus.get_status_lbl(status)
1588 1589 if status and allowed_to_change_status else None),
1589 1590 status_change_type=(status
1590 1591 if status and allowed_to_change_status else None),
1591 1592 comment_type=comment_type,
1592 1593 is_draft=is_draft,
1593 1594 resolves_comment_id=resolves_comment_id,
1594 1595 auth_user=self._rhodecode_user,
1595 1596 send_email=not is_draft, # skip notification for draft comments
1596 1597 )
1597 1598 is_inline = comment.is_inline
1598 1599
1599 1600 if allowed_to_change_status:
1600 1601 # calculate old status before we change it
1601 1602 old_calculated_status = pull_request.calculated_review_status()
1602 1603
1603 1604 # get status if set !
1604 1605 if status:
1605 1606 ChangesetStatusModel().set_status(
1606 1607 self.db_repo.repo_id,
1607 1608 status,
1608 1609 self._rhodecode_user.user_id,
1609 1610 comment,
1610 1611 pull_request=pull_request
1611 1612 )
1612 1613
1613 1614 Session().flush()
1614 1615 # this is somehow required to get access to some relationship
1615 1616 # loaded on comment
1616 1617 Session().refresh(comment)
1617 1618
1618 1619 PullRequestModel().trigger_pull_request_hook(
1619 1620 pull_request, self._rhodecode_user, 'comment',
1620 1621 data={'comment': comment})
1621 1622
1622 1623 # we now calculate the status of pull request, and based on that
1623 1624 # calculation we set the commits status
1624 1625 calculated_status = pull_request.calculated_review_status()
1625 1626 if old_calculated_status != calculated_status:
1626 1627 PullRequestModel().trigger_pull_request_hook(
1627 1628 pull_request, self._rhodecode_user, 'review_status_change',
1628 1629 data={'status': calculated_status})
1629 1630
1630 1631 comment_id = comment.comment_id
1631 1632 data[comment_id] = {
1632 1633 'target_id': target_elem_id
1633 1634 }
1634 1635 Session().flush()
1635 1636
1636 1637 c.co = comment
1637 1638 c.at_version_num = None
1638 1639 c.is_new = True
1639 1640 rendered_comment = render(
1640 1641 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 1642 self._get_template_context(c), self.request)
1642 1643
1643 1644 data[comment_id].update(comment.get_dict())
1644 1645 data[comment_id].update({'rendered_text': rendered_comment})
1645 1646
1646 1647 Session().commit()
1647 1648
1648 1649 # skip channelstream for draft comments
1649 1650 if all_drafts:
1650 1651 comment_broadcast_channel = channelstream.comment_channel(
1651 1652 self.db_repo_name, pull_request_obj=pull_request)
1652 1653
1653 1654 comment_data = data
1654 1655 comment_type = 'inline' if is_inline else 'general'
1655 1656 if len(data) == 1:
1656 1657 msg = _('posted {} new {} comment').format(len(data), comment_type)
1657 1658 else:
1658 1659 msg = _('posted {} new {} comments').format(len(data), comment_type)
1659 1660
1660 1661 channelstream.comment_channelstream_push(
1661 1662 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 1663 comment_data=comment_data)
1663 1664
1664 1665 return data
1665 1666
1666 1667 @LoginRequired()
1667 1668 @NotAnonymous()
1668 1669 @HasRepoPermissionAnyDecorator(
1669 1670 'repository.read', 'repository.write', 'repository.admin')
1670 1671 @CSRFRequired()
1671 1672 @view_config(
1672 1673 route_name='pullrequest_comment_create', request_method='POST',
1673 1674 renderer='json_ext')
1674 1675 def pull_request_comment_create(self):
1675 1676 _ = self.request.translate
1676 1677
1677 1678 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678 1679
1679 1680 if pull_request.is_closed():
1680 1681 log.debug('comment: forbidden because pull request is closed')
1681 1682 raise HTTPForbidden()
1682 1683
1683 1684 allowed_to_comment = PullRequestModel().check_user_comment(
1684 1685 pull_request, self._rhodecode_user)
1685 1686 if not allowed_to_comment:
1686 1687 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 1688 raise HTTPForbidden()
1688 1689
1689 1690 comment_data = {
1690 1691 'comment_type': self.request.POST.get('comment_type'),
1691 1692 'text': self.request.POST.get('text'),
1692 1693 'status': self.request.POST.get('changeset_status', None),
1693 1694 'is_draft': self.request.POST.get('draft'),
1694 1695 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 1696 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 1697 'f_path': self.request.POST.get('f_path'),
1697 1698 'line': self.request.POST.get('line'),
1698 1699 }
1699 1700 data = self._pull_request_comments_create(pull_request, [comment_data])
1700 1701
1701 1702 return data
1702 1703
1703 1704 @LoginRequired()
1704 1705 @NotAnonymous()
1705 1706 @HasRepoPermissionAnyDecorator(
1706 1707 'repository.read', 'repository.write', 'repository.admin')
1707 1708 @CSRFRequired()
1708 1709 @view_config(
1709 1710 route_name='pullrequest_comment_delete', request_method='POST',
1710 1711 renderer='json_ext')
1711 1712 def pull_request_comment_delete(self):
1712 1713 pull_request = PullRequest.get_or_404(
1713 1714 self.request.matchdict['pull_request_id'])
1714 1715
1715 1716 comment = ChangesetComment.get_or_404(
1716 1717 self.request.matchdict['comment_id'])
1717 1718 comment_id = comment.comment_id
1718 1719
1719 1720 if comment.immutable:
1720 1721 # don't allow deleting comments that are immutable
1721 1722 raise HTTPForbidden()
1722 1723
1723 1724 if pull_request.is_closed():
1724 1725 log.debug('comment: forbidden because pull request is closed')
1725 1726 raise HTTPForbidden()
1726 1727
1727 1728 if not comment:
1728 1729 log.debug('Comment with id:%s not found, skipping', comment_id)
1729 1730 # comment already deleted in another call probably
1730 1731 return True
1731 1732
1732 1733 if comment.pull_request.is_closed():
1733 1734 # don't allow deleting comments on closed pull request
1734 1735 raise HTTPForbidden()
1735 1736
1736 1737 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1737 1738 super_admin = h.HasPermissionAny('hg.admin')()
1738 1739 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1739 1740 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1740 1741 comment_repo_admin = is_repo_admin and is_repo_comment
1741 1742
1742 1743 if super_admin or comment_owner or comment_repo_admin:
1743 1744 old_calculated_status = comment.pull_request.calculated_review_status()
1744 1745 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1745 1746 Session().commit()
1746 1747 calculated_status = comment.pull_request.calculated_review_status()
1747 1748 if old_calculated_status != calculated_status:
1748 1749 PullRequestModel().trigger_pull_request_hook(
1749 1750 comment.pull_request, self._rhodecode_user, 'review_status_change',
1750 1751 data={'status': calculated_status})
1751 1752 return True
1752 1753 else:
1753 1754 log.warning('No permissions for user %s to delete comment_id: %s',
1754 1755 self._rhodecode_db_user, comment_id)
1755 1756 raise HTTPNotFound()
1756 1757
1757 1758 @LoginRequired()
1758 1759 @NotAnonymous()
1759 1760 @HasRepoPermissionAnyDecorator(
1760 1761 'repository.read', 'repository.write', 'repository.admin')
1761 1762 @CSRFRequired()
1762 1763 @view_config(
1763 1764 route_name='pullrequest_comment_edit', request_method='POST',
1764 1765 renderer='json_ext')
1765 1766 def pull_request_comment_edit(self):
1766 1767 self.load_default_context()
1767 1768
1768 1769 pull_request = PullRequest.get_or_404(
1769 1770 self.request.matchdict['pull_request_id']
1770 1771 )
1771 1772 comment = ChangesetComment.get_or_404(
1772 1773 self.request.matchdict['comment_id']
1773 1774 )
1774 1775 comment_id = comment.comment_id
1775 1776
1776 1777 if comment.immutable:
1777 1778 # don't allow deleting comments that are immutable
1778 1779 raise HTTPForbidden()
1779 1780
1780 1781 if pull_request.is_closed():
1781 1782 log.debug('comment: forbidden because pull request is closed')
1782 1783 raise HTTPForbidden()
1783 1784
1784 1785 if not comment:
1785 1786 log.debug('Comment with id:%s not found, skipping', comment_id)
1786 1787 # comment already deleted in another call probably
1787 1788 return True
1788 1789
1789 1790 if comment.pull_request.is_closed():
1790 1791 # don't allow deleting comments on closed pull request
1791 1792 raise HTTPForbidden()
1792 1793
1793 1794 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1794 1795 super_admin = h.HasPermissionAny('hg.admin')()
1795 1796 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1796 1797 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1797 1798 comment_repo_admin = is_repo_admin and is_repo_comment
1798 1799
1799 1800 if super_admin or comment_owner or comment_repo_admin:
1800 1801 text = self.request.POST.get('text')
1801 1802 version = self.request.POST.get('version')
1802 1803 if text == comment.text:
1803 1804 log.warning(
1804 1805 'Comment(PR): '
1805 1806 'Trying to create new version '
1806 1807 'with the same comment body {}'.format(
1807 1808 comment_id,
1808 1809 )
1809 1810 )
1810 1811 raise HTTPNotFound()
1811 1812
1812 1813 if version.isdigit():
1813 1814 version = int(version)
1814 1815 else:
1815 1816 log.warning(
1816 1817 'Comment(PR): Wrong version type {} {} '
1817 1818 'for comment {}'.format(
1818 1819 version,
1819 1820 type(version),
1820 1821 comment_id,
1821 1822 )
1822 1823 )
1823 1824 raise HTTPNotFound()
1824 1825
1825 1826 try:
1826 1827 comment_history = CommentsModel().edit(
1827 1828 comment_id=comment_id,
1828 1829 text=text,
1829 1830 auth_user=self._rhodecode_user,
1830 1831 version=version,
1831 1832 )
1832 1833 except CommentVersionMismatch:
1833 1834 raise HTTPConflict()
1834 1835
1835 1836 if not comment_history:
1836 1837 raise HTTPNotFound()
1837 1838
1838 1839 Session().commit()
1839 1840
1840 1841 PullRequestModel().trigger_pull_request_hook(
1841 1842 pull_request, self._rhodecode_user, 'comment_edit',
1842 1843 data={'comment': comment})
1843 1844
1844 1845 return {
1845 1846 'comment_history_id': comment_history.comment_history_id,
1846 1847 'comment_id': comment.comment_id,
1847 1848 'comment_version': comment_history.version,
1848 1849 'comment_author_username': comment_history.author.username,
1849 1850 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1850 1851 'comment_created_on': h.age_component(comment_history.created_on,
1851 1852 time_is_local=True),
1852 1853 }
1853 1854 else:
1854 1855 log.warning('No permissions for user %s to edit comment_id: %s',
1855 1856 self._rhodecode_db_user, comment_id)
1856 1857 raise HTTPNotFound()
@@ -1,843 +1,846 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24 import datetime
25 25
26 26 import logging
27 27 import traceback
28 28 import collections
29 29
30 30 from pyramid.threadlocal import get_current_registry, get_current_request
31 31 from sqlalchemy.sql.expression import null
32 32 from sqlalchemy.sql.functions import coalesce
33 33
34 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 35 from rhodecode.lib import audit_logger
36 36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (
40 40 false,
41 41 ChangesetComment,
42 42 User,
43 43 Notification,
44 44 PullRequest,
45 45 AttributeDict,
46 46 ChangesetCommentHistory,
47 47 )
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51 from rhodecode.model.notification import EmailNotificationModel
52 52 from rhodecode.model.validation_schema.schemas import comment_schema
53 53
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class CommentsModel(BaseModel):
59 59
60 60 cls = ChangesetComment
61 61
62 62 DIFF_CONTEXT_BEFORE = 3
63 63 DIFF_CONTEXT_AFTER = 3
64 64
65 65 def __get_commit_comment(self, changeset_comment):
66 66 return self._get_instance(ChangesetComment, changeset_comment)
67 67
68 68 def __get_pull_request(self, pull_request):
69 69 return self._get_instance(PullRequest, pull_request)
70 70
71 71 def _extract_mentions(self, s):
72 72 user_objects = []
73 73 for username in extract_mentioned_users(s):
74 74 user_obj = User.get_by_username(username, case_insensitive=True)
75 75 if user_obj:
76 76 user_objects.append(user_obj)
77 77 return user_objects
78 78
79 79 def _get_renderer(self, global_renderer='rst', request=None):
80 80 request = request or get_current_request()
81 81
82 82 try:
83 83 global_renderer = request.call_context.visual.default_renderer
84 84 except AttributeError:
85 85 log.debug("Renderer not set, falling back "
86 86 "to default renderer '%s'", global_renderer)
87 87 except Exception:
88 88 log.error(traceback.format_exc())
89 89 return global_renderer
90 90
91 91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 92 # group by versions, and count until, and display objects
93 93
94 94 comment_groups = collections.defaultdict(list)
95 95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96 96
97 97 def yield_comments(pos):
98 98 for co in comment_groups[pos]:
99 99 yield co
100 100
101 101 comment_versions = collections.defaultdict(
102 102 lambda: collections.defaultdict(list))
103 103 prev_prvid = -1
104 104 # fake last entry with None, to aggregate on "latest" version which
105 105 # doesn't have an pull_request_version_id
106 106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 107 prvid = ver.pull_request_version_id
108 108 if prev_prvid == -1:
109 109 prev_prvid = prvid
110 110
111 111 for co in yield_comments(prvid):
112 112 comment_versions[prvid]['at'].append(co)
113 113
114 114 # save until
115 115 current = comment_versions[prvid]['at']
116 116 prev_until = comment_versions[prev_prvid]['until']
117 117 cur_until = prev_until + current
118 118 comment_versions[prvid]['until'].extend(cur_until)
119 119
120 120 # save outdated
121 121 if inline:
122 122 outdated = [x for x in cur_until
123 123 if x.outdated_at_version(show_version)]
124 124 else:
125 125 outdated = [x for x in cur_until
126 126 if x.older_than_version(show_version)]
127 127 display = [x for x in cur_until if x not in outdated]
128 128
129 129 comment_versions[prvid]['outdated'] = outdated
130 130 comment_versions[prvid]['display'] = display
131 131
132 132 prev_prvid = prvid
133 133
134 134 return comment_versions
135 135
136 136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 137 qry = Session().query(ChangesetComment) \
138 138 .filter(ChangesetComment.repo == repo)
139 139
140 140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142 142
143 143 if user:
144 144 user = self._get_user(user)
145 145 if user:
146 146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147 147
148 148 if commit_id:
149 149 qry = qry.filter(ChangesetComment.revision == commit_id)
150 150
151 151 qry = qry.order_by(ChangesetComment.created_on)
152 152 return qry.all()
153 153
154 154 def get_repository_unresolved_todos(self, repo):
155 155 todos = Session().query(ChangesetComment) \
156 156 .filter(ChangesetComment.repo == repo) \
157 157 .filter(ChangesetComment.resolved_by == None) \
158 158 .filter(ChangesetComment.comment_type
159 159 == ChangesetComment.COMMENT_TYPE_TODO)
160 160 todos = todos.all()
161 161
162 162 return todos
163 163
164 164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165 165
166 166 todos = Session().query(ChangesetComment) \
167 167 .filter(ChangesetComment.pull_request == pull_request) \
168 168 .filter(ChangesetComment.resolved_by == None) \
169 169 .filter(ChangesetComment.comment_type
170 170 == ChangesetComment.COMMENT_TYPE_TODO)
171 171
172 172 if not include_drafts:
173 173 todos = todos.filter(ChangesetComment.draft == false())
174 174
175 175 if not show_outdated:
176 176 todos = todos.filter(
177 177 coalesce(ChangesetComment.display_state, '') !=
178 178 ChangesetComment.COMMENT_OUTDATED)
179 179
180 180 todos = todos.all()
181 181
182 182 return todos
183 183
184 184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185 185
186 186 todos = Session().query(ChangesetComment) \
187 187 .filter(ChangesetComment.pull_request == pull_request) \
188 188 .filter(ChangesetComment.resolved_by != None) \
189 189 .filter(ChangesetComment.comment_type
190 190 == ChangesetComment.COMMENT_TYPE_TODO)
191 191
192 192 if not include_drafts:
193 193 todos = todos.filter(ChangesetComment.draft == false())
194 194
195 195 if not show_outdated:
196 196 todos = todos.filter(
197 197 coalesce(ChangesetComment.display_state, '') !=
198 198 ChangesetComment.COMMENT_OUTDATED)
199 199
200 200 todos = todos.all()
201 201
202 202 return todos
203 203
204 204 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
205 205
206 206 todos = Session().query(ChangesetComment) \
207 207 .filter(ChangesetComment.revision == commit_id) \
208 208 .filter(ChangesetComment.resolved_by == None) \
209 209 .filter(ChangesetComment.comment_type
210 210 == ChangesetComment.COMMENT_TYPE_TODO)
211 211
212 212 if not include_drafts:
213 213 todos = todos.filter(ChangesetComment.draft == false())
214 214
215 215 if not show_outdated:
216 216 todos = todos.filter(
217 217 coalesce(ChangesetComment.display_state, '') !=
218 218 ChangesetComment.COMMENT_OUTDATED)
219 219
220 220 todos = todos.all()
221 221
222 222 return todos
223 223
224 224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
225 225
226 226 todos = Session().query(ChangesetComment) \
227 227 .filter(ChangesetComment.revision == commit_id) \
228 228 .filter(ChangesetComment.resolved_by != None) \
229 229 .filter(ChangesetComment.comment_type
230 230 == ChangesetComment.COMMENT_TYPE_TODO)
231 231
232 232 if not include_drafts:
233 233 todos = todos.filter(ChangesetComment.draft == false())
234 234
235 235 if not show_outdated:
236 236 todos = todos.filter(
237 237 coalesce(ChangesetComment.display_state, '') !=
238 238 ChangesetComment.COMMENT_OUTDATED)
239 239
240 240 todos = todos.all()
241 241
242 242 return todos
243 243
244 244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
245 245 inline_comments = Session().query(ChangesetComment) \
246 246 .filter(ChangesetComment.line_no != None) \
247 247 .filter(ChangesetComment.f_path != None) \
248 248 .filter(ChangesetComment.revision == commit_id)
249 249
250 250 if not include_drafts:
251 251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252 252
253 253 inline_comments = inline_comments.all()
254 254 return inline_comments
255 255
256 256 def _log_audit_action(self, action, action_data, auth_user, comment):
257 257 audit_logger.store(
258 258 action=action,
259 259 action_data=action_data,
260 260 user=auth_user,
261 261 repo=comment.repo)
262 262
263 263 def create(self, text, repo, user, commit_id=None, pull_request=None,
264 264 f_path=None, line_no=None, status_change=None,
265 265 status_change_type=None, comment_type=None, is_draft=False,
266 266 resolves_comment_id=None, closing_pr=False, send_email=True,
267 267 renderer=None, auth_user=None, extra_recipients=None):
268 268 """
269 269 Creates new comment for commit or pull request.
270 270 IF status_change is not none this comment is associated with a
271 271 status change of commit or commit associated with pull request
272 272
273 273 :param text:
274 274 :param repo:
275 275 :param user:
276 276 :param commit_id:
277 277 :param pull_request:
278 278 :param f_path:
279 279 :param line_no:
280 280 :param status_change: Label for status change
281 281 :param comment_type: Type of comment
282 282 :param is_draft: is comment a draft only
283 283 :param resolves_comment_id: id of comment which this one will resolve
284 284 :param status_change_type: type of status change
285 285 :param closing_pr:
286 286 :param send_email:
287 287 :param renderer: pick renderer for this comment
288 288 :param auth_user: current authenticated user calling this method
289 289 :param extra_recipients: list of extra users to be added to recipients
290 290 """
291 291
292 292 if not text:
293 293 log.warning('Missing text for comment, skipping...')
294 294 return
295 295 request = get_current_request()
296 296 _ = request.translate
297 297
298 298 if not renderer:
299 299 renderer = self._get_renderer(request=request)
300 300
301 301 repo = self._get_repo(repo)
302 302 user = self._get_user(user)
303 303 auth_user = auth_user or user
304 304
305 305 schema = comment_schema.CommentSchema()
306 306 validated_kwargs = schema.deserialize(dict(
307 307 comment_body=text,
308 308 comment_type=comment_type,
309 309 is_draft=is_draft,
310 310 comment_file=f_path,
311 311 comment_line=line_no,
312 312 renderer_type=renderer,
313 313 status_change=status_change_type,
314 314 resolves_comment_id=resolves_comment_id,
315 315 repo=repo.repo_id,
316 316 user=user.user_id,
317 317 ))
318 318 is_draft = validated_kwargs['is_draft']
319 319
320 320 comment = ChangesetComment()
321 321 comment.renderer = validated_kwargs['renderer_type']
322 322 comment.text = validated_kwargs['comment_body']
323 323 comment.f_path = validated_kwargs['comment_file']
324 324 comment.line_no = validated_kwargs['comment_line']
325 325 comment.comment_type = validated_kwargs['comment_type']
326 326 comment.draft = is_draft
327 327
328 328 comment.repo = repo
329 329 comment.author = user
330 330 resolved_comment = self.__get_commit_comment(
331 331 validated_kwargs['resolves_comment_id'])
332 332 # check if the comment actually belongs to this PR
333 333 if resolved_comment and resolved_comment.pull_request and \
334 334 resolved_comment.pull_request != pull_request:
335 335 log.warning('Comment tried to resolved unrelated todo comment: %s',
336 336 resolved_comment)
337 337 # comment not bound to this pull request, forbid
338 338 resolved_comment = None
339 339
340 340 elif resolved_comment and resolved_comment.repo and \
341 341 resolved_comment.repo != repo:
342 342 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 343 resolved_comment)
344 344 # comment not bound to this repo, forbid
345 345 resolved_comment = None
346 346
347 347 comment.resolved_comment = resolved_comment
348 348
349 349 pull_request_id = pull_request
350 350
351 351 commit_obj = None
352 352 pull_request_obj = None
353 353
354 354 if commit_id:
355 355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
356 356 # do a lookup, so we don't pass something bad here
357 357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
358 358 comment.revision = commit_obj.raw_id
359 359
360 360 elif pull_request_id:
361 361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
362 362 pull_request_obj = self.__get_pull_request(pull_request_id)
363 363 comment.pull_request = pull_request_obj
364 364 else:
365 365 raise Exception('Please specify commit or pull_request_id')
366 366
367 367 Session().add(comment)
368 368 Session().flush()
369 369 kwargs = {
370 370 'user': user,
371 371 'renderer_type': renderer,
372 372 'repo_name': repo.repo_name,
373 373 'status_change': status_change,
374 374 'status_change_type': status_change_type,
375 375 'comment_body': text,
376 376 'comment_file': f_path,
377 377 'comment_line': line_no,
378 378 'comment_type': comment_type or 'note',
379 379 'comment_id': comment.comment_id
380 380 }
381 381
382 382 if commit_obj:
383 383 recipients = ChangesetComment.get_users(
384 384 revision=commit_obj.raw_id)
385 385 # add commit author if it's in RhodeCode system
386 386 cs_author = User.get_from_cs_author(commit_obj.author)
387 387 if not cs_author:
388 388 # use repo owner if we cannot extract the author correctly
389 389 cs_author = repo.user
390 390 recipients += [cs_author]
391 391
392 392 commit_comment_url = self.get_url(comment, request=request)
393 393 commit_comment_reply_url = self.get_url(
394 394 comment, request=request,
395 395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
396 396
397 397 target_repo_url = h.link_to(
398 398 repo.repo_name,
399 399 h.route_url('repo_summary', repo_name=repo.repo_name))
400 400
401 401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
402 402 commit_id=commit_id)
403 403
404 404 # commit specifics
405 405 kwargs.update({
406 406 'commit': commit_obj,
407 407 'commit_message': commit_obj.message,
408 408 'commit_target_repo_url': target_repo_url,
409 409 'commit_comment_url': commit_comment_url,
410 410 'commit_comment_reply_url': commit_comment_reply_url,
411 411 'commit_url': commit_url,
412 412 'thread_ids': [commit_url, commit_comment_url],
413 413 })
414 414
415 415 elif pull_request_obj:
416 416 # get the current participants of this pull request
417 417 recipients = ChangesetComment.get_users(
418 418 pull_request_id=pull_request_obj.pull_request_id)
419 419 # add pull request author
420 420 recipients += [pull_request_obj.author]
421 421
422 422 # add the reviewers to notification
423 423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
424 424
425 425 pr_target_repo = pull_request_obj.target_repo
426 426 pr_source_repo = pull_request_obj.source_repo
427 427
428 428 pr_comment_url = self.get_url(comment, request=request)
429 429 pr_comment_reply_url = self.get_url(
430 430 comment, request=request,
431 431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
432 432
433 433 pr_url = h.route_url(
434 434 'pullrequest_show',
435 435 repo_name=pr_target_repo.repo_name,
436 436 pull_request_id=pull_request_obj.pull_request_id, )
437 437
438 438 # set some variables for email notification
439 439 pr_target_repo_url = h.route_url(
440 440 'repo_summary', repo_name=pr_target_repo.repo_name)
441 441
442 442 pr_source_repo_url = h.route_url(
443 443 'repo_summary', repo_name=pr_source_repo.repo_name)
444 444
445 445 # pull request specifics
446 446 kwargs.update({
447 447 'pull_request': pull_request_obj,
448 448 'pr_id': pull_request_obj.pull_request_id,
449 449 'pull_request_url': pr_url,
450 450 'pull_request_target_repo': pr_target_repo,
451 451 'pull_request_target_repo_url': pr_target_repo_url,
452 452 'pull_request_source_repo': pr_source_repo,
453 453 'pull_request_source_repo_url': pr_source_repo_url,
454 454 'pr_comment_url': pr_comment_url,
455 455 'pr_comment_reply_url': pr_comment_reply_url,
456 456 'pr_closing': closing_pr,
457 457 'thread_ids': [pr_url, pr_comment_url],
458 458 })
459 459
460 460 if send_email:
461 461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
462 462 # pre-generate the subject for notification itself
463 463 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
464 464 notification_type, **kwargs)
465 465
466 466 mention_recipients = set(
467 467 self._extract_mentions(text)).difference(recipients)
468 468
469 469 # create notification objects, and emails
470 470 NotificationModel().create(
471 471 created_by=user,
472 472 notification_subject=subject,
473 473 notification_body=body_plaintext,
474 474 notification_type=notification_type,
475 475 recipients=recipients,
476 476 mention_recipients=mention_recipients,
477 477 email_kwargs=kwargs,
478 478 )
479 479
480 480 Session().flush()
481 481 if comment.pull_request:
482 482 action = 'repo.pull_request.comment.create'
483 483 else:
484 484 action = 'repo.commit.comment.create'
485 485
486 486 if not is_draft:
487 487 comment_data = comment.get_api_data()
488 488
489 489 self._log_audit_action(
490 490 action, {'data': comment_data}, auth_user, comment)
491 491
492 492 return comment
493 493
494 494 def edit(self, comment_id, text, auth_user, version):
495 495 """
496 496 Change existing comment for commit or pull request.
497 497
498 498 :param comment_id:
499 499 :param text:
500 500 :param auth_user: current authenticated user calling this method
501 501 :param version: last comment version
502 502 """
503 503 if not text:
504 504 log.warning('Missing text for comment, skipping...')
505 505 return
506 506
507 507 comment = ChangesetComment.get(comment_id)
508 508 old_comment_text = comment.text
509 509 comment.text = text
510 510 comment.modified_at = datetime.datetime.now()
511 511 version = safe_int(version)
512 512
513 513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 514 # would return 3 here
515 515 comment_version = ChangesetCommentHistory.get_version(comment_id)
516 516
517 517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 518 log.warning(
519 519 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 520 comment_version-1, # -1 since note above
521 521 version
522 522 )
523 523 )
524 524 raise CommentVersionMismatch()
525 525
526 526 comment_history = ChangesetCommentHistory()
527 527 comment_history.comment_id = comment_id
528 528 comment_history.version = comment_version
529 529 comment_history.created_by_user_id = auth_user.user_id
530 530 comment_history.text = old_comment_text
531 531 # TODO add email notification
532 532 Session().add(comment_history)
533 533 Session().add(comment)
534 534 Session().flush()
535 535
536 536 if comment.pull_request:
537 537 action = 'repo.pull_request.comment.edit'
538 538 else:
539 539 action = 'repo.commit.comment.edit'
540 540
541 541 comment_data = comment.get_api_data()
542 542 comment_data['old_comment_text'] = old_comment_text
543 543 self._log_audit_action(
544 544 action, {'data': comment_data}, auth_user, comment)
545 545
546 546 return comment_history
547 547
548 548 def delete(self, comment, auth_user):
549 549 """
550 550 Deletes given comment
551 551 """
552 552 comment = self.__get_commit_comment(comment)
553 553 old_data = comment.get_api_data()
554 554 Session().delete(comment)
555 555
556 556 if comment.pull_request:
557 557 action = 'repo.pull_request.comment.delete'
558 558 else:
559 559 action = 'repo.commit.comment.delete'
560 560
561 561 self._log_audit_action(
562 562 action, {'old_data': old_data}, auth_user, comment)
563 563
564 564 return comment
565 565
566 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
566 def get_all_comments(self, repo_id, revision=None, pull_request=None,
567 include_drafts=True, count_only=False):
567 568 q = ChangesetComment.query()\
568 569 .filter(ChangesetComment.repo_id == repo_id)
569 570 if revision:
570 571 q = q.filter(ChangesetComment.revision == revision)
571 572 elif pull_request:
572 573 pull_request = self.__get_pull_request(pull_request)
573 574 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
574 575 else:
575 576 raise Exception('Please specify commit or pull_request')
577 if not include_drafts:
578 q = q.filter(ChangesetComment.draft == false())
576 579 q = q.order_by(ChangesetComment.created_on)
577 580 if count_only:
578 581 return q.count()
579 582
580 583 return q.all()
581 584
582 585 def get_url(self, comment, request=None, permalink=False, anchor=None):
583 586 if not request:
584 587 request = get_current_request()
585 588
586 589 comment = self.__get_commit_comment(comment)
587 590 if anchor is None:
588 591 anchor = 'comment-{}'.format(comment.comment_id)
589 592
590 593 if comment.pull_request:
591 594 pull_request = comment.pull_request
592 595 if permalink:
593 596 return request.route_url(
594 597 'pull_requests_global',
595 598 pull_request_id=pull_request.pull_request_id,
596 599 _anchor=anchor)
597 600 else:
598 601 return request.route_url(
599 602 'pullrequest_show',
600 603 repo_name=safe_str(pull_request.target_repo.repo_name),
601 604 pull_request_id=pull_request.pull_request_id,
602 605 _anchor=anchor)
603 606
604 607 else:
605 608 repo = comment.repo
606 609 commit_id = comment.revision
607 610
608 611 if permalink:
609 612 return request.route_url(
610 613 'repo_commit', repo_name=safe_str(repo.repo_id),
611 614 commit_id=commit_id,
612 615 _anchor=anchor)
613 616
614 617 else:
615 618 return request.route_url(
616 619 'repo_commit', repo_name=safe_str(repo.repo_name),
617 620 commit_id=commit_id,
618 621 _anchor=anchor)
619 622
620 623 def get_comments(self, repo_id, revision=None, pull_request=None):
621 624 """
622 625 Gets main comments based on revision or pull_request_id
623 626
624 627 :param repo_id:
625 628 :param revision:
626 629 :param pull_request:
627 630 """
628 631
629 632 q = ChangesetComment.query()\
630 633 .filter(ChangesetComment.repo_id == repo_id)\
631 634 .filter(ChangesetComment.line_no == None)\
632 635 .filter(ChangesetComment.f_path == None)
633 636 if revision:
634 637 q = q.filter(ChangesetComment.revision == revision)
635 638 elif pull_request:
636 639 pull_request = self.__get_pull_request(pull_request)
637 640 q = q.filter(ChangesetComment.pull_request == pull_request)
638 641 else:
639 642 raise Exception('Please specify commit or pull_request')
640 643 q = q.order_by(ChangesetComment.created_on)
641 644 return q.all()
642 645
643 646 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
644 647 q = self._get_inline_comments_query(repo_id, revision, pull_request)
645 648 return self._group_comments_by_path_and_line_number(q)
646 649
647 650 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
648 651 version=None):
649 652 inline_comms = []
650 653 for fname, per_line_comments in inline_comments.iteritems():
651 654 for lno, comments in per_line_comments.iteritems():
652 655 for comm in comments:
653 656 if not comm.outdated_at_version(version) and skip_outdated:
654 657 inline_comms.append(comm)
655 658
656 659 return inline_comms
657 660
658 661 def get_outdated_comments(self, repo_id, pull_request):
659 662 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
660 663 # of a pull request.
661 664 q = self._all_inline_comments_of_pull_request(pull_request)
662 665 q = q.filter(
663 666 ChangesetComment.display_state ==
664 667 ChangesetComment.COMMENT_OUTDATED
665 668 ).order_by(ChangesetComment.comment_id.asc())
666 669
667 670 return self._group_comments_by_path_and_line_number(q)
668 671
669 672 def _get_inline_comments_query(self, repo_id, revision, pull_request):
670 673 # TODO: johbo: Split this into two methods: One for PR and one for
671 674 # commit.
672 675 if revision:
673 676 q = Session().query(ChangesetComment).filter(
674 677 ChangesetComment.repo_id == repo_id,
675 678 ChangesetComment.line_no != null(),
676 679 ChangesetComment.f_path != null(),
677 680 ChangesetComment.revision == revision)
678 681
679 682 elif pull_request:
680 683 pull_request = self.__get_pull_request(pull_request)
681 684 if not CommentsModel.use_outdated_comments(pull_request):
682 685 q = self._visible_inline_comments_of_pull_request(pull_request)
683 686 else:
684 687 q = self._all_inline_comments_of_pull_request(pull_request)
685 688
686 689 else:
687 690 raise Exception('Please specify commit or pull_request_id')
688 691 q = q.order_by(ChangesetComment.comment_id.asc())
689 692 return q
690 693
691 694 def _group_comments_by_path_and_line_number(self, q):
692 695 comments = q.all()
693 696 paths = collections.defaultdict(lambda: collections.defaultdict(list))
694 697 for co in comments:
695 698 paths[co.f_path][co.line_no].append(co)
696 699 return paths
697 700
698 701 @classmethod
699 702 def needed_extra_diff_context(cls):
700 703 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
701 704
702 705 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
703 706 if not CommentsModel.use_outdated_comments(pull_request):
704 707 return
705 708
706 709 comments = self._visible_inline_comments_of_pull_request(pull_request)
707 710 comments_to_outdate = comments.all()
708 711
709 712 for comment in comments_to_outdate:
710 713 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
711 714
712 715 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
713 716 diff_line = _parse_comment_line_number(comment.line_no)
714 717
715 718 try:
716 719 old_context = old_diff_proc.get_context_of_line(
717 720 path=comment.f_path, diff_line=diff_line)
718 721 new_context = new_diff_proc.get_context_of_line(
719 722 path=comment.f_path, diff_line=diff_line)
720 723 except (diffs.LineNotInDiffException,
721 724 diffs.FileNotInDiffException):
722 725 comment.display_state = ChangesetComment.COMMENT_OUTDATED
723 726 return
724 727
725 728 if old_context == new_context:
726 729 return
727 730
728 731 if self._should_relocate_diff_line(diff_line):
729 732 new_diff_lines = new_diff_proc.find_context(
730 733 path=comment.f_path, context=old_context,
731 734 offset=self.DIFF_CONTEXT_BEFORE)
732 735 if not new_diff_lines:
733 736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
734 737 else:
735 738 new_diff_line = self._choose_closest_diff_line(
736 739 diff_line, new_diff_lines)
737 740 comment.line_no = _diff_to_comment_line_number(new_diff_line)
738 741 else:
739 742 comment.display_state = ChangesetComment.COMMENT_OUTDATED
740 743
741 744 def _should_relocate_diff_line(self, diff_line):
742 745 """
743 746 Checks if relocation shall be tried for the given `diff_line`.
744 747
745 748 If a comment points into the first lines, then we can have a situation
746 749 that after an update another line has been added on top. In this case
747 750 we would find the context still and move the comment around. This
748 751 would be wrong.
749 752 """
750 753 should_relocate = (
751 754 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
752 755 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
753 756 return should_relocate
754 757
755 758 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
756 759 candidate = new_diff_lines[0]
757 760 best_delta = _diff_line_delta(diff_line, candidate)
758 761 for new_diff_line in new_diff_lines[1:]:
759 762 delta = _diff_line_delta(diff_line, new_diff_line)
760 763 if delta < best_delta:
761 764 candidate = new_diff_line
762 765 best_delta = delta
763 766 return candidate
764 767
765 768 def _visible_inline_comments_of_pull_request(self, pull_request):
766 769 comments = self._all_inline_comments_of_pull_request(pull_request)
767 770 comments = comments.filter(
768 771 coalesce(ChangesetComment.display_state, '') !=
769 772 ChangesetComment.COMMENT_OUTDATED)
770 773 return comments
771 774
772 775 def _all_inline_comments_of_pull_request(self, pull_request):
773 776 comments = Session().query(ChangesetComment)\
774 777 .filter(ChangesetComment.line_no != None)\
775 778 .filter(ChangesetComment.f_path != None)\
776 779 .filter(ChangesetComment.pull_request == pull_request)
777 780 return comments
778 781
779 782 def _all_general_comments_of_pull_request(self, pull_request):
780 783 comments = Session().query(ChangesetComment)\
781 784 .filter(ChangesetComment.line_no == None)\
782 785 .filter(ChangesetComment.f_path == None)\
783 786 .filter(ChangesetComment.pull_request == pull_request)
784 787
785 788 return comments
786 789
787 790 @staticmethod
788 791 def use_outdated_comments(pull_request):
789 792 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
790 793 settings = settings_model.get_general_settings()
791 794 return settings.get('rhodecode_use_outdated_comments', False)
792 795
793 796 def trigger_commit_comment_hook(self, repo, user, action, data=None):
794 797 repo = self._get_repo(repo)
795 798 target_scm = repo.scm_instance()
796 799 if action == 'create':
797 800 trigger_hook = hooks_utils.trigger_comment_commit_hooks
798 801 elif action == 'edit':
799 802 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
800 803 else:
801 804 return
802 805
803 806 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
804 807 repo, action, trigger_hook)
805 808 trigger_hook(
806 809 username=user.username,
807 810 repo_name=repo.repo_name,
808 811 repo_type=target_scm.alias,
809 812 repo=repo,
810 813 data=data)
811 814
812 815
813 816 def _parse_comment_line_number(line_no):
814 817 """
815 818 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
816 819 """
817 820 old_line = None
818 821 new_line = None
819 822 if line_no.startswith('o'):
820 823 old_line = int(line_no[1:])
821 824 elif line_no.startswith('n'):
822 825 new_line = int(line_no[1:])
823 826 else:
824 827 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
825 828 return diffs.DiffLineNumber(old_line, new_line)
826 829
827 830
828 831 def _diff_to_comment_line_number(diff_line):
829 832 if diff_line.new is not None:
830 833 return u'n{}'.format(diff_line.new)
831 834 elif diff_line.old is not None:
832 835 return u'o{}'.format(diff_line.old)
833 836 return u''
834 837
835 838
836 839 def _diff_line_delta(a, b):
837 840 if None not in (a.new, b.new):
838 841 return abs(a.new - b.new)
839 842 elif None not in (a.old, b.old):
840 843 return abs(a.old - b.old)
841 844 else:
842 845 raise ValueError(
843 846 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1415 +1,1415 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
3 3
4 4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
5 5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
6 6 %></%def>
7 7
8 8 <%def name="action_class(action)">
9 9 <%
10 10 return {
11 11 '-': 'cb-deletion',
12 12 '+': 'cb-addition',
13 13 ' ': 'cb-context',
14 14 }.get(action, 'cb-empty')
15 15 %>
16 16 </%def>
17 17
18 18 <%def name="op_class(op_id)">
19 19 <%
20 20 return {
21 21 DEL_FILENODE: 'deletion', # file deleted
22 22 BIN_FILENODE: 'warning' # binary diff hidden
23 23 }.get(op_id, 'addition')
24 24 %>
25 25 </%def>
26 26
27 27
28 28
29 29 <%def name="render_diffset(diffset, commit=None,
30 30
31 31 # collapse all file diff entries when there are more than this amount of files in the diff
32 32 collapse_when_files_over=20,
33 33
34 34 # collapse lines in the diff when more than this amount of lines changed in the file diff
35 35 lines_changed_limit=500,
36 36
37 37 # add a ruler at to the output
38 38 ruler_at_chars=0,
39 39
40 40 # show inline comments
41 41 use_comments=False,
42 42
43 43 # disable new comments
44 44 disable_new_comments=False,
45 45
46 46 # special file-comments that were deleted in previous versions
47 47 # it's used for showing outdated comments for deleted files in a PR
48 48 deleted_files_comments=None,
49 49
50 50 # for cache purpose
51 51 inline_comments=None,
52 52
53 53 # additional menu for PRs
54 54 pull_request_menu=None,
55 55
56 56 # show/hide todo next to comments
57 57 show_todos=True,
58 58
59 59 )">
60 60
61 61 <%
62 62 diffset_container_id = h.md5(diffset.target_ref)
63 63 collapse_all = len(diffset.files) > collapse_when_files_over
64 64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
65 65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
66 66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
67 67 %>
68 68
69 69 %if use_comments:
70 70
71 71 ## Template for injecting comments
72 72 <div id="cb-comments-inline-container-template" class="js-template">
73 73 ${inline_comments_container([])}
74 74 </div>
75 75
76 76 <div class="js-template" id="cb-comment-inline-form-template">
77 77 <div class="comment-inline-form ac">
78 78 %if not c.rhodecode_user.is_default:
79 79 ## render template for inline comments
80 80 ${commentblock.comment_form(form_type='inline')}
81 81 %endif
82 82 </div>
83 83 </div>
84 84
85 85 %endif
86 86
87 87 %if c.user_session_attrs["diffmode"] == 'sideside':
88 88 <style>
89 89 .wrapper {
90 90 max-width: 1600px !important;
91 91 }
92 92 </style>
93 93 %endif
94 94
95 95 %if ruler_at_chars:
96 96 <style>
97 97 .diff table.cb .cb-content:after {
98 98 content: "";
99 99 border-left: 1px solid blue;
100 100 position: absolute;
101 101 top: 0;
102 102 height: 18px;
103 103 opacity: .2;
104 104 z-index: 10;
105 105 //## +5 to account for diff action (+/-)
106 106 left: ${ruler_at_chars + 5}ch;
107 107 </style>
108 108 %endif
109 109
110 110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
111 111
112 112 <div style="height: 20px; line-height: 20px">
113 113 ## expand/collapse action
114 114 <div class="pull-left">
115 115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
116 116 % if collapse_all:
117 117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
118 118 % else:
119 119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
120 120 % endif
121 121 </a>
122 122
123 123 </div>
124 124
125 125 ## todos
126 126 % if show_todos and getattr(c, 'at_version', None):
127 127 <div class="pull-right">
128 128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
129 129 ${_('not available in this view')}
130 130 </div>
131 131 % elif show_todos:
132 132 <div class="pull-right">
133 133 <div class="comments-number" style="padding-left: 10px">
134 134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
135 135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
136 136 % if c.unresolved_comments:
137 137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
138 138 ${_('{} unresolved').format(len(c.unresolved_comments))}
139 139 </a>
140 140 % else:
141 141 ${_('0 unresolved')}
142 142 % endif
143 143
144 144 ${_('{} Resolved').format(len(c.resolved_comments))}
145 145 % endif
146 146 </div>
147 147 </div>
148 148 % endif
149 149
150 150 ## ## comments
151 151 ## <div class="pull-right">
152 152 ## <div class="comments-number" style="padding-left: 10px">
153 153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
154 154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
155 155 ## % if c.comments:
156 156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
157 157 ## % else:
158 158 ## ${_('0 General')}
159 159 ## % endif
160 160 ##
161 161 ## % if c.inline_cnt:
162 162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
163 163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
164 164 ## </a>
165 165 ## % else:
166 166 ## ${_('0 Inline')}
167 167 ## % endif
168 168 ## % endif
169 169 ##
170 170 ## % if pull_request_menu:
171 171 ## <%
172 172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
173 173 ## %>
174 174 ##
175 175 ## % if outdated_comm_count_ver:
176 176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
177 177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
178 178 ## </a>
179 179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
180 180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
181 181 ## % else:
182 182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
183 183 ## % endif
184 184 ##
185 185 ## % endif
186 186 ##
187 187 ## </div>
188 188 ## </div>
189 189
190 190 </div>
191 191
192 192 % if diffset.limited_diff:
193 193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
194 194 <h2 class="clearinner">
195 195 ${_('The requested changes are too big and content was truncated.')}
196 196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
197 197 </h2>
198 198 </div>
199 199 % endif
200 200
201 201 <div id="todo-box">
202 202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
203 203 % for co in c.unresolved_comments:
204 204 <a class="permalink" href="#comment-${co.comment_id}"
205 205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
206 206 <i class="icon-flag-filled-red"></i>
207 207 ${co.comment_id}</a>${('' if loop.last else ',')}
208 208 % endfor
209 209 % endif
210 210 </div>
211 211 %if diffset.has_hidden_changes:
212 212 <p class="empty_data">${_('Some changes may be hidden')}</p>
213 213 %elif not diffset.files:
214 214 <p class="empty_data">${_('No files')}</p>
215 215 %endif
216 216
217 217 <div class="filediffs">
218 218
219 219 ## initial value could be marked as False later on
220 220 <% over_lines_changed_limit = False %>
221 221 %for i, filediff in enumerate(diffset.files):
222 222
223 223 %if filediff.source_file_path and filediff.target_file_path:
224 224 %if filediff.source_file_path != filediff.target_file_path:
225 225 ## file was renamed, or copied
226 226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
227 227 <%
228 228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
229 229 final_path = filediff.target_file_path
230 230 %>
231 231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
232 232 <%
233 233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
234 234 final_path = filediff.target_file_path
235 235 %>
236 236 %endif
237 237 %else:
238 238 ## file was modified
239 239 <%
240 240 final_file_name = filediff.source_file_path
241 241 final_path = final_file_name
242 242 %>
243 243 %endif
244 244 %else:
245 245 %if filediff.source_file_path:
246 246 ## file was deleted
247 247 <%
248 248 final_file_name = filediff.source_file_path
249 249 final_path = final_file_name
250 250 %>
251 251 %else:
252 252 ## file was added
253 253 <%
254 254 final_file_name = filediff.target_file_path
255 255 final_path = final_file_name
256 256 %>
257 257 %endif
258 258 %endif
259 259
260 260 <%
261 261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
262 262 over_lines_changed_limit = lines_changed > lines_changed_limit
263 263 %>
264 264 ## anchor with support of sticky header
265 265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
266 266
267 267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
268 268 <div
269 269 class="filediff"
270 270 data-f-path="${filediff.patch['filename']}"
271 271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
272 272 >
273 273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
274 274 <%
275 275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
277 277 %>
278 278 <div class="filediff-collapse-indicator icon-"></div>
279 279
280 280 ## Comments/Options PILL
281 281 <span class="pill-group pull-right">
282 282 <span class="pill" op="comments">
283 283 <i class="icon-comment"></i> ${len(total_file_comments)}
284 284 </span>
285 285
286 286 <details class="details-reset details-inline-block">
287 287 <summary class="noselect">
288 288 <i class="pill icon-options cursor-pointer" op="options"></i>
289 289 </summary>
290 290 <details-menu class="details-dropdown">
291 291
292 292 <div class="dropdown-item">
293 293 <span>${final_path}</span>
294 294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
295 295 </div>
296 296
297 297 <div class="dropdown-divider"></div>
298 298
299 299 <div class="dropdown-item">
300 300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
301 301 <a href="${permalink}">ΒΆ permalink</a>
302 302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
303 303 </div>
304 304
305 305
306 306 </details-menu>
307 307 </details>
308 308
309 309 </span>
310 310
311 311 ${diff_ops(final_file_name, filediff)}
312 312
313 313 </label>
314 314
315 315 ${diff_menu(filediff, use_comments=use_comments)}
316 316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
317 317
318 318 ## new/deleted/empty content case
319 319 % if not filediff.hunks:
320 320 ## Comment container, on "fakes" hunk that contains all data to render comments
321 321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
322 322 % endif
323 323
324 324 %if filediff.limited_diff:
325 325 <tr class="cb-warning cb-collapser">
326 326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
327 327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
328 328 </td>
329 329 </tr>
330 330 %else:
331 331 %if over_lines_changed_limit:
332 332 <tr class="cb-warning cb-collapser">
333 333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
334 334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
335 335 <a href="#" class="cb-expand"
336 336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
337 337 </a>
338 338 <a href="#" class="cb-collapse"
339 339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
340 340 </a>
341 341 </td>
342 342 </tr>
343 343 %endif
344 344 %endif
345 345
346 346 % for hunk in filediff.hunks:
347 347 <tr class="cb-hunk">
348 348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
349 349 ## TODO: dan: add ajax loading of more context here
350 350 ## <a href="#">
351 351 <i class="icon-more"></i>
352 352 ## </a>
353 353 </td>
354 354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
355 355 @@
356 356 -${hunk.source_start},${hunk.source_length}
357 357 +${hunk.target_start},${hunk.target_length}
358 358 ${hunk.section_header}
359 359 </td>
360 360 </tr>
361 361
362 362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
363 363 % endfor
364 364
365 365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
366 366
367 367 ## outdated comments that do not fit into currently displayed lines
368 368 % for lineno, comments in unmatched_comments.items():
369 369
370 370 %if c.user_session_attrs["diffmode"] == 'unified':
371 371 % if loop.index == 0:
372 372 <tr class="cb-hunk">
373 373 <td colspan="3"></td>
374 374 <td>
375 375 <div>
376 376 ${_('Unmatched/outdated inline comments below')}
377 377 </div>
378 378 </td>
379 379 </tr>
380 380 % endif
381 381 <tr class="cb-line">
382 382 <td class="cb-data cb-context"></td>
383 383 <td class="cb-lineno cb-context"></td>
384 384 <td class="cb-lineno cb-context"></td>
385 385 <td class="cb-content cb-context">
386 386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
387 387 </td>
388 388 </tr>
389 389 %elif c.user_session_attrs["diffmode"] == 'sideside':
390 390 % if loop.index == 0:
391 391 <tr class="cb-comment-info">
392 392 <td colspan="2"></td>
393 393 <td class="cb-line">
394 394 <div>
395 395 ${_('Unmatched/outdated inline comments below')}
396 396 </div>
397 397 </td>
398 398 <td colspan="2"></td>
399 399 <td class="cb-line">
400 400 <div>
401 401 ${_('Unmatched/outdated comments below')}
402 402 </div>
403 403 </td>
404 404 </tr>
405 405 % endif
406 406 <tr class="cb-line">
407 407 <td class="cb-data cb-context"></td>
408 408 <td class="cb-lineno cb-context"></td>
409 409 <td class="cb-content cb-context">
410 410 % if lineno.startswith('o'):
411 411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
412 412 % endif
413 413 </td>
414 414
415 415 <td class="cb-data cb-context"></td>
416 416 <td class="cb-lineno cb-context"></td>
417 417 <td class="cb-content cb-context">
418 418 % if lineno.startswith('n'):
419 419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
420 420 % endif
421 421 </td>
422 422 </tr>
423 423 %endif
424 424
425 425 % endfor
426 426
427 427 </table>
428 428 </div>
429 429 %endfor
430 430
431 431 ## outdated comments that are made for a file that has been deleted
432 432 % for filename, comments_dict in (deleted_files_comments or {}).items():
433 433
434 434 <%
435 435 display_state = 'display: none'
436 436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
437 437 if open_comments_in_file:
438 438 display_state = ''
439 439 fid = str(id(filename))
440 440 %>
441 441 <div class="filediffs filediff-outdated" style="${display_state}">
442 442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
443 443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
444 444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
445 445 <div class="filediff-collapse-indicator icon-"></div>
446 446
447 447 <span class="pill">
448 448 ## file was deleted
449 449 ${filename}
450 450 </span>
451 451 <span class="pill-group pull-left" >
452 452 ## file op, doesn't need translation
453 453 <span class="pill" op="removed">unresolved comments</span>
454 454 </span>
455 455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
456 456 <span class="pill-group pull-right">
457 457 <span class="pill" op="deleted">
458 458 % if comments_dict['stats'] >0:
459 459 -${comments_dict['stats']}
460 460 % else:
461 461 ${comments_dict['stats']}
462 462 % endif
463 463 </span>
464 464 </span>
465 465 </label>
466 466
467 467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
468 468 <tr>
469 469 % if c.user_session_attrs["diffmode"] == 'unified':
470 470 <td></td>
471 471 %endif
472 472
473 473 <td></td>
474 474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
475 475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
476 476 ${_('There are still outdated/unresolved comments attached to it.')}
477 477 </td>
478 478 </tr>
479 479 %if c.user_session_attrs["diffmode"] == 'unified':
480 480 <tr class="cb-line">
481 481 <td class="cb-data cb-context"></td>
482 482 <td class="cb-lineno cb-context"></td>
483 483 <td class="cb-lineno cb-context"></td>
484 484 <td class="cb-content cb-context">
485 485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
486 486 </td>
487 487 </tr>
488 488 %elif c.user_session_attrs["diffmode"] == 'sideside':
489 489 <tr class="cb-line">
490 490 <td class="cb-data cb-context"></td>
491 491 <td class="cb-lineno cb-context"></td>
492 492 <td class="cb-content cb-context"></td>
493 493
494 494 <td class="cb-data cb-context"></td>
495 495 <td class="cb-lineno cb-context"></td>
496 496 <td class="cb-content cb-context">
497 497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
498 498 </td>
499 499 </tr>
500 500 %endif
501 501 </table>
502 502 </div>
503 503 </div>
504 504 % endfor
505 505
506 506 </div>
507 507 </div>
508 508 </%def>
509 509
510 510 <%def name="diff_ops(file_name, filediff)">
511 511 <%
512 512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
513 513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
514 514 %>
515 515 <span class="pill">
516 516 <i class="icon-file-text"></i>
517 517 ${file_name}
518 518 </span>
519 519
520 520 <span class="pill-group pull-right">
521 521
522 522 ## ops pills
523 523 %if filediff.limited_diff:
524 524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
525 525 %endif
526 526
527 527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
528 528 <span class="pill" op="created">created</span>
529 529 %if filediff['target_mode'].startswith('120'):
530 530 <span class="pill" op="symlink">symlink</span>
531 531 %else:
532 532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
533 533 %endif
534 534 %endif
535 535
536 536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
537 537 <span class="pill" op="renamed">renamed</span>
538 538 %endif
539 539
540 540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
541 541 <span class="pill" op="copied">copied</span>
542 542 %endif
543 543
544 544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
545 545 <span class="pill" op="removed">removed</span>
546 546 %endif
547 547
548 548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
549 549 <span class="pill" op="mode">
550 550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
551 551 </span>
552 552 %endif
553 553
554 554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
555 555 <span class="pill" op="binary">binary</span>
556 556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
557 557 <span class="pill" op="modified">modified</span>
558 558 %endif
559 559 %endif
560 560
561 561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
562 562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
563 563
564 564 </span>
565 565
566 566 </%def>
567 567
568 568 <%def name="nice_mode(filemode)">
569 569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
570 570 </%def>
571 571
572 572 <%def name="diff_menu(filediff, use_comments=False)">
573 573 <div class="filediff-menu">
574 574
575 575 %if filediff.diffset.source_ref:
576 576
577 577 ## FILE BEFORE CHANGES
578 578 %if filediff.operation in ['D', 'M']:
579 579 <a
580 580 class="tooltip"
581 581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
582 582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
583 583 >
584 584 ${_('Show file before')}
585 585 </a> |
586 586 %else:
587 587 <span
588 588 class="tooltip"
589 589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
590 590 >
591 591 ${_('Show file before')}
592 592 </span> |
593 593 %endif
594 594
595 595 ## FILE AFTER CHANGES
596 596 %if filediff.operation in ['A', 'M']:
597 597 <a
598 598 class="tooltip"
599 599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
600 600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
601 601 >
602 602 ${_('Show file after')}
603 603 </a>
604 604 %else:
605 605 <span
606 606 class="tooltip"
607 607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
608 608 >
609 609 ${_('Show file after')}
610 610 </span>
611 611 %endif
612 612
613 613 % if use_comments:
614 614 |
615 615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 616 data-toggle-on="${_('Hide comments')}"
617 617 data-toggle-off="${_('Show comments')}">
618 618 <span class="hide-comment-button">${_('Hide comments')}</span>
619 619 </a>
620 620 % endif
621 621
622 622 %endif
623 623
624 624 </div>
625 625 </%def>
626 626
627 627
628 628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
629 629
630 630 <div class="inline-comments">
631 631 %for comment in comments:
632 632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
633 633 %endfor
634 634
635 635 <%
636 636 extra_class = ''
637 637 extra_style = ''
638 638
639 639 if comments and comments[-1].outdated:
640 640 extra_class = ' comment-outdated'
641 641 extra_style = 'display: none;'
642 642
643 643 %>
644 644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
645 645 <div class="reply-thread-container${extra_class}">
646 646 <div class="reply-thread-gravatar">
647 647 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
648 648 </div>
649 649 <div class="reply-thread-reply-button">
650 650 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
651 651 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
652 652 </div>
653 653 <div class="reply-thread-last"></div>
654 654 </div>
655 655 </div>
656 656 </div>
657 657
658 658 </%def>
659 659
660 660 <%!
661 661
662 662 def get_inline_comments(comments, filename):
663 663 if hasattr(filename, 'unicode_path'):
664 664 filename = filename.unicode_path
665 665
666 666 if not isinstance(filename, (unicode, str)):
667 667 return None
668 668
669 669 if comments and filename in comments:
670 670 return comments[filename]
671 671
672 672 return None
673 673
674 674 def get_comments_for(diff_type, comments, filename, line_version, line_number):
675 675 if hasattr(filename, 'unicode_path'):
676 676 filename = filename.unicode_path
677 677
678 678 if not isinstance(filename, (unicode, str)):
679 679 return None
680 680
681 681 file_comments = get_inline_comments(comments, filename)
682 682 if file_comments is None:
683 683 return None
684 684
685 685 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
686 686 if line_key in file_comments:
687 687 data = file_comments.pop(line_key)
688 688 return data
689 689 %>
690 690
691 691 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
692 692
693 693 <% chunk_count = 1 %>
694 694 %for loop_obj, item in h.looper(hunk.sideside):
695 695 <%
696 696 line = item
697 697 i = loop_obj.index
698 698 prev_line = loop_obj.previous
699 699 old_line_anchor, new_line_anchor = None, None
700 700
701 701 if line.original.lineno:
702 702 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
703 703 if line.modified.lineno:
704 704 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
705 705
706 706 line_action = line.modified.action or line.original.action
707 707 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
708 708 %>
709 709
710 710 <tr class="cb-line">
711 711 <td class="cb-data ${action_class(line.original.action)}"
712 712 data-line-no="${line.original.lineno}"
713 713 >
714 714
715 715 <% line_old_comments, line_old_comments_no_drafts = None, None %>
716 716 %if line.original.get_comment_args:
717 717 <%
718 718 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
719 719 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
720 720 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
721 721 %>
722 722 %endif
723 723 %if line_old_comments_no_drafts:
724 724 % if has_outdated:
725 725 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
726 726 % else:
727 727 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
728 728 % endif
729 729 %endif
730 730 </td>
731 731 <td class="cb-lineno ${action_class(line.original.action)}"
732 732 data-line-no="${line.original.lineno}"
733 733 %if old_line_anchor:
734 734 id="${old_line_anchor}"
735 735 %endif
736 736 >
737 737 %if line.original.lineno:
738 738 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
739 739 %endif
740 740 </td>
741 741
742 742 <% line_no = 'o{}'.format(line.original.lineno) %>
743 743 <td class="cb-content ${action_class(line.original.action)}"
744 744 data-line-no="${line_no}"
745 745 >
746 746 %if use_comments and line.original.lineno:
747 747 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
748 748 %endif
749 749 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
750 750
751 751 %if use_comments and line.original.lineno and line_old_comments:
752 752 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
753 753 %endif
754 754
755 755 </td>
756 756 <td class="cb-data ${action_class(line.modified.action)}"
757 757 data-line-no="${line.modified.lineno}"
758 758 >
759 759 <div>
760 760
761 761 <% line_new_comments, line_new_comments_no_drafts = None, None %>
762 762 %if line.modified.get_comment_args:
763 763 <%
764 764 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
765 765 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
766 766 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
767 767 %>
768 768 %endif
769 769
770 770 %if line_new_comments_no_drafts:
771 771 % if has_outdated:
772 772 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
773 773 % else:
774 774 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
775 775 % endif
776 776 %endif
777 777 </div>
778 778 </td>
779 779 <td class="cb-lineno ${action_class(line.modified.action)}"
780 780 data-line-no="${line.modified.lineno}"
781 781 %if new_line_anchor:
782 782 id="${new_line_anchor}"
783 783 %endif
784 784 >
785 785 %if line.modified.lineno:
786 786 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
787 787 %endif
788 788 </td>
789 789
790 790 <% line_no = 'n{}'.format(line.modified.lineno) %>
791 791 <td class="cb-content ${action_class(line.modified.action)}"
792 792 data-line-no="${line_no}"
793 793 >
794 794 %if use_comments and line.modified.lineno:
795 795 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
796 796 %endif
797 797 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
798 798 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
799 799 <div class="nav-chunk" style="visibility: hidden">
800 800 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
801 801 </div>
802 802 <% chunk_count +=1 %>
803 803 % endif
804 804 %if use_comments and line.modified.lineno and line_new_comments:
805 805 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
806 806 %endif
807 807
808 808 </td>
809 809 </tr>
810 810 %endfor
811 811 </%def>
812 812
813 813
814 814 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
815 815 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
816 816
817 817 <%
818 818 old_line_anchor, new_line_anchor = None, None
819 819 if old_line_no:
820 820 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
821 821 if new_line_no:
822 822 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
823 823 %>
824 824 <tr class="cb-line">
825 825 <td class="cb-data ${action_class(action)}">
826 826 <div>
827 827
828 828 <% comments, comments_no_drafts = None, None %>
829 829 %if comments_args:
830 830 <%
831 831 comments = get_comments_for('unified', inline_comments, *comments_args)
832 832 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
833 833 has_outdated = any([x.outdated for x in comments_no_drafts])
834 834 %>
835 835 %endif
836 836
837 837 % if comments_no_drafts:
838 838 % if has_outdated:
839 839 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
840 840 % else:
841 841 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
842 842 % endif
843 843 % endif
844 844 </div>
845 845 </td>
846 846 <td class="cb-lineno ${action_class(action)}"
847 847 data-line-no="${old_line_no}"
848 848 %if old_line_anchor:
849 849 id="${old_line_anchor}"
850 850 %endif
851 851 >
852 852 %if old_line_anchor:
853 853 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
854 854 %endif
855 855 </td>
856 856 <td class="cb-lineno ${action_class(action)}"
857 857 data-line-no="${new_line_no}"
858 858 %if new_line_anchor:
859 859 id="${new_line_anchor}"
860 860 %endif
861 861 >
862 862 %if new_line_anchor:
863 863 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
864 864 %endif
865 865 </td>
866 866 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
867 867 <td class="cb-content ${action_class(action)}"
868 868 data-line-no="${line_no}"
869 869 >
870 870 %if use_comments:
871 871 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
872 872 %endif
873 873 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
874 874 %if use_comments and comments:
875 875 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
876 876 %endif
877 877 </td>
878 878 </tr>
879 879 %endfor
880 880 </%def>
881 881
882 882
883 883 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
884 884 % if diff_mode == 'unified':
885 885 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
886 886 % elif diff_mode == 'sideside':
887 887 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
888 888 % else:
889 889 <tr class="cb-line">
890 890 <td>unknown diff mode</td>
891 891 </tr>
892 892 % endif
893 893 </%def>file changes
894 894
895 895
896 896 <%def name="render_add_comment_button(line_no='', f_path='')">
897 897 % if not c.rhodecode_user.is_default:
898 898 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
899 899 <span><i class="icon-comment"></i></span>
900 900 </button>
901 901 % endif
902 902 </%def>
903 903
904 904 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
905 905 <% diffset_container_id = h.md5(diffset.target_ref) %>
906 906
907 907 <div id="diff-file-sticky" class="diffset-menu clearinner">
908 908 ## auto adjustable
909 909 <div class="sidebar__inner">
910 910 <div class="sidebar__bar">
911 911 <div class="pull-right">
912 912 <div class="btn-group">
913 913 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
914 914 <i class="icon-wide-mode"></i>
915 915 </a>
916 916 </div>
917 917 <div class="btn-group">
918 918
919 919 <a
920 920 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
921 921 title="${h.tooltip(_('View diff as side by side'))}"
922 922 href="${h.current_route_path(request, diffmode='sideside')}">
923 923 <span>${_('Side by Side')}</span>
924 924 </a>
925 925
926 926 <a
927 927 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
928 928 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
929 929 <span>${_('Unified')}</span>
930 930 </a>
931 931
932 932 % if range_diff_on is True:
933 933 <a
934 934 title="${_('Turn off: Show the diff as commit range')}"
935 935 class="btn btn-primary"
936 936 href="${h.current_route_path(request, **{"range-diff":"0"})}">
937 937 <span>${_('Range Diff')}</span>
938 938 </a>
939 939 % elif range_diff_on is False:
940 940 <a
941 941 title="${_('Show the diff as commit range')}"
942 942 class="btn"
943 943 href="${h.current_route_path(request, **{"range-diff":"1"})}">
944 944 <span>${_('Range Diff')}</span>
945 945 </a>
946 946 % endif
947 947 </div>
948 948 <div class="btn-group">
949 949
950 950 <div class="pull-left">
951 951 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
952 952 </div>
953 953
954 954 </div>
955 955 </div>
956 956 <div class="pull-left">
957 957 <div class="btn-group">
958 958 <div class="pull-left">
959 959 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
960 960 </div>
961 961
962 962 </div>
963 963 </div>
964 964 </div>
965 965 <div class="fpath-placeholder pull-left">
966 966 <i class="icon-file-text"></i>
967 967 <strong class="fpath-placeholder-text">
968 968 Context file:
969 969 </strong>
970 970 </div>
971 971 <div class="pull-right noselect">
972 972
973 973 %if commit:
974 974 <span>
975 975 <code>${h.show_id(commit)}</code>
976 976 </span>
977 977 %elif pull_request_menu and pull_request_menu.get('pull_request'):
978 978 <span>
979 979 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
980 980 </span>
981 981 %endif
982 982 % if commit or pull_request_menu:
983 983 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
984 984 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
985 985 <i class="icon-angle-up"></i>
986 986 </span>
987 987 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
988 988 <i class="icon-angle-down"></i>
989 989 </span>
990 990 % endif
991 991 </div>
992 992 <div class="sidebar_inner_shadow"></div>
993 993 </div>
994 994 </div>
995 995
996 996 % if diffset:
997 997 %if diffset.limited_diff:
998 998 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
999 999 %else:
1000 1000 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1001 1001 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1002 1002
1003 1003 %endif
1004 1004 ## case on range-diff placeholder needs to be updated
1005 1005 % if range_diff_on is True:
1006 1006 <% file_placeholder = _('Disabled on range diff') %>
1007 1007 % endif
1008 1008
1009 1009 <script type="text/javascript">
1010 1010 var feedFilesOptions = function (query, initialData) {
1011 1011 var data = {results: []};
1012 1012 var isQuery = typeof query.term !== 'undefined';
1013 1013
1014 1014 var section = _gettext('Changed files');
1015 1015 var filteredData = [];
1016 1016
1017 1017 //filter results
1018 1018 $.each(initialData.results, function (idx, value) {
1019 1019
1020 1020 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1021 1021 filteredData.push({
1022 1022 'id': this.id,
1023 1023 'text': this.text,
1024 1024 "ops": this.ops,
1025 1025 })
1026 1026 }
1027 1027
1028 1028 });
1029 1029
1030 1030 data.results = filteredData;
1031 1031
1032 1032 query.callback(data);
1033 1033 };
1034 1034
1035 1035 var selectionFormatter = function(data, escapeMarkup) {
1036 1036 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1037 1037 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1038 1038 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1039 1039 '<span class="pill" op="added">{0}</span>' +
1040 1040 '<span class="pill" op="deleted">{1}</span>' +
1041 1041 '</div>'
1042 1042 ;
1043 1043 var added = data['ops']['added'];
1044 1044 if (added === 0) {
1045 1045 // don't show +0
1046 1046 added = 0;
1047 1047 } else {
1048 1048 added = '+' + added;
1049 1049 }
1050 1050
1051 1051 var deleted = -1*data['ops']['deleted'];
1052 1052
1053 1053 tmpl += pill.format(added, deleted);
1054 1054 return container.format(tmpl);
1055 1055 };
1056 1056 var formatFileResult = function(result, container, query, escapeMarkup) {
1057 1057 return selectionFormatter(result, escapeMarkup);
1058 1058 };
1059 1059
1060 1060 var formatSelection = function (data, container) {
1061 1061 return '${file_placeholder}'
1062 1062 };
1063 1063
1064 1064 if (window.preloadFileFilterData === undefined) {
1065 1065 window.preloadFileFilterData = {}
1066 1066 }
1067 1067
1068 1068 preloadFileFilterData["${diffset_container_id}"] = {
1069 1069 results: [
1070 1070 % for filediff in diffset.files:
1071 1071 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1072 1072 text:"${filediff.patch['filename']}",
1073 1073 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1074 1074 % endfor
1075 1075 ]
1076 1076 };
1077 1077
1078 1078 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1079 1079 var diffFileFilter = $(diffFileFilterId).select2({
1080 1080 'dropdownAutoWidth': true,
1081 1081 'width': 'auto',
1082 1082
1083 1083 containerCssClass: "drop-menu",
1084 1084 dropdownCssClass: "drop-menu-dropdown",
1085 1085 data: preloadFileFilterData["${diffset_container_id}"],
1086 1086 query: function(query) {
1087 1087 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1088 1088 },
1089 1089 initSelection: function(element, callback) {
1090 1090 callback({'init': true});
1091 1091 },
1092 1092 formatResult: formatFileResult,
1093 1093 formatSelection: formatSelection
1094 1094 });
1095 1095
1096 1096 % if range_diff_on is True:
1097 1097 diffFileFilter.select2("enable", false);
1098 1098 % endif
1099 1099
1100 1100 $(diffFileFilterId).on('select2-selecting', function (e) {
1101 1101 var idSelector = e.choice.id;
1102 1102
1103 1103 // expand the container if we quick-select the field
1104 1104 $('#'+idSelector).next().prop('checked', false);
1105 1105 // hide the mast as we later do preventDefault()
1106 1106 $("#select2-drop-mask").click();
1107 1107
1108 1108 window.location.hash = '#'+idSelector;
1109 1109 updateSticky();
1110 1110
1111 1111 e.preventDefault();
1112 1112 });
1113 1113
1114 1114 diffNavText = 'diff navigation:'
1115 1115
1116 1116 getCurrentChunk = function () {
1117 1117
1118 1118 var chunksAll = $('.nav-chunk').filter(function () {
1119 1119 return $(this).parents('.filediff').prev().get(0).checked !== true
1120 1120 })
1121 1121 var chunkSelected = $('.nav-chunk.selected');
1122 1122 var initial = false;
1123 1123
1124 1124 if (chunkSelected.length === 0) {
1125 1125 // no initial chunk selected, we pick first
1126 1126 chunkSelected = $(chunksAll.get(0));
1127 1127 var initial = true;
1128 1128 }
1129 1129
1130 1130 return {
1131 1131 'all': chunksAll,
1132 1132 'selected': chunkSelected,
1133 1133 'initial': initial,
1134 1134 }
1135 1135 }
1136 1136
1137 1137 animateDiffNavText = function () {
1138 1138 var $diffNav = $('#diff_nav')
1139 1139
1140 1140 var callback = function () {
1141 1141 $diffNav.animate({'opacity': 1.00}, 200)
1142 1142 };
1143 1143 $diffNav.animate({'opacity': 0.15}, 200, callback);
1144 1144 }
1145 1145
1146 1146 scrollToChunk = function (moveBy) {
1147 1147 var chunk = getCurrentChunk();
1148 1148 var all = chunk.all
1149 1149 var selected = chunk.selected
1150 1150
1151 1151 var curPos = all.index(selected);
1152 1152 var newPos = curPos;
1153 1153 if (!chunk.initial) {
1154 1154 var newPos = curPos + moveBy;
1155 1155 }
1156 1156
1157 1157 var curElem = all.get(newPos);
1158 1158
1159 1159 if (curElem === undefined) {
1160 1160 // end or back
1161 1161 $('#diff_nav').html('no next diff element:')
1162 1162 animateDiffNavText()
1163 1163 return
1164 1164 } else if (newPos < 0) {
1165 1165 $('#diff_nav').html('no previous diff element:')
1166 1166 animateDiffNavText()
1167 1167 return
1168 1168 } else {
1169 1169 $('#diff_nav').html(diffNavText)
1170 1170 }
1171 1171
1172 1172 curElem = $(curElem)
1173 1173 var offset = 100;
1174 1174 $(window).scrollTop(curElem.position().top - offset);
1175 1175
1176 1176 //clear selection
1177 1177 all.removeClass('selected')
1178 1178 curElem.addClass('selected')
1179 1179 }
1180 1180
1181 1181 scrollToPrevChunk = function () {
1182 1182 scrollToChunk(-1)
1183 1183 }
1184 1184 scrollToNextChunk = function () {
1185 1185 scrollToChunk(1)
1186 1186 }
1187 1187
1188 1188 </script>
1189 1189 % endif
1190 1190
1191 1191 <script type="text/javascript">
1192 1192 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1193 1193
1194 1194 $(document).ready(function () {
1195 1195
1196 1196 var contextPrefix = _gettext('Context file: ');
1197 1197 ## sticky sidebar
1198 1198 var sidebarElement = document.getElementById('diff-file-sticky');
1199 1199 sidebar = new StickySidebar(sidebarElement, {
1200 1200 topSpacing: 0,
1201 1201 bottomSpacing: 0,
1202 1202 innerWrapperSelector: '.sidebar__inner'
1203 1203 });
1204 1204 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1205 1205 // reset our file so it's not holding new value
1206 1206 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1207 1207 });
1208 1208
1209 1209 updateSticky = function () {
1210 1210 sidebar.updateSticky();
1211 1211 Waypoint.refreshAll();
1212 1212 };
1213 1213
1214 1214 var animateText = function (fPath, anchorId) {
1215 1215 fPath = Select2.util.escapeMarkup(fPath);
1216 1216 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1217 1217 };
1218 1218
1219 1219 ## dynamic file waypoints
1220 1220 var setFPathInfo = function(fPath, anchorId){
1221 1221 animateText(fPath, anchorId)
1222 1222 };
1223 1223
1224 1224 var codeBlock = $('.filediff');
1225 1225
1226 1226 // forward waypoint
1227 1227 codeBlock.waypoint(
1228 1228 function(direction) {
1229 1229 if (direction === "down"){
1230 1230 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1231 1231 }
1232 1232 }, {
1233 1233 offset: function () {
1234 1234 return 70;
1235 1235 },
1236 1236 context: '.fpath-placeholder'
1237 1237 }
1238 1238 );
1239 1239
1240 1240 // backward waypoint
1241 1241 codeBlock.waypoint(
1242 1242 function(direction) {
1243 1243 if (direction === "up"){
1244 1244 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1245 1245 }
1246 1246 }, {
1247 1247 offset: function () {
1248 1248 return -this.element.clientHeight + 90;
1249 1249 },
1250 1250 context: '.fpath-placeholder'
1251 1251 }
1252 1252 );
1253 1253
1254 1254 toggleWideDiff = function (el) {
1255 1255 updateSticky();
1256 1256 var wide = Rhodecode.comments.toggleWideMode(this);
1257 1257 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1258 1258 if (wide === true) {
1259 1259 $(el).addClass('btn-active');
1260 1260 } else {
1261 1261 $(el).removeClass('btn-active');
1262 1262 }
1263 1263 return null;
1264 1264 };
1265 1265
1266 1266 var preloadDiffMenuData = {
1267 1267 results: [
1268 1268
1269 1269 ## Whitespace change
1270 1270 % if request.GET.get('ignorews', '') == '1':
1271 1271 {
1272 1272 id: 2,
1273 1273 text: _gettext('Show whitespace changes'),
1274 1274 action: function () {},
1275 1275 url: "${h.current_route_path(request, ignorews=0)|n}"
1276 1276 },
1277 1277 % else:
1278 1278 {
1279 1279 id: 2,
1280 1280 text: _gettext('Hide whitespace changes'),
1281 1281 action: function () {},
1282 1282 url: "${h.current_route_path(request, ignorews=1)|n}"
1283 1283 },
1284 1284 % endif
1285 1285
1286 1286 ## FULL CONTEXT
1287 1287 % if request.GET.get('fullcontext', '') == '1':
1288 1288 {
1289 1289 id: 3,
1290 1290 text: _gettext('Hide full context diff'),
1291 1291 action: function () {},
1292 1292 url: "${h.current_route_path(request, fullcontext=0)|n}"
1293 1293 },
1294 1294 % else:
1295 1295 {
1296 1296 id: 3,
1297 1297 text: _gettext('Show full context diff'),
1298 1298 action: function () {},
1299 1299 url: "${h.current_route_path(request, fullcontext=1)|n}"
1300 1300 },
1301 1301 % endif
1302 1302
1303 1303 ]
1304 1304 };
1305 1305
1306 1306 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1307 1307 $(diffMenuId).select2({
1308 1308 minimumResultsForSearch: -1,
1309 1309 containerCssClass: "drop-menu-no-width",
1310 1310 dropdownCssClass: "drop-menu-dropdown",
1311 1311 dropdownAutoWidth: true,
1312 1312 data: preloadDiffMenuData,
1313 1313 placeholder: "${_('...')}",
1314 1314 });
1315 1315 $(diffMenuId).on('select2-selecting', function (e) {
1316 1316 e.choice.action();
1317 1317 if (e.choice.url !== null) {
1318 1318 window.location = e.choice.url
1319 1319 }
1320 1320 });
1321 1321 toggleExpand = function (el, diffsetEl) {
1322 1322 var el = $(el);
1323 1323 if (el.hasClass('collapsed')) {
1324 1324 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1325 1325 el.removeClass('collapsed');
1326 1326 el.html(
1327 1327 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1328 1328 _gettext('Collapse all files'));
1329 1329 }
1330 1330 else {
1331 1331 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1332 1332 el.addClass('collapsed');
1333 1333 el.html(
1334 1334 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1335 1335 _gettext('Expand all files'));
1336 1336 }
1337 1337 updateSticky()
1338 1338 };
1339 1339
1340 1340 toggleCommitExpand = function (el) {
1341 1341 var $el = $(el);
1342 1342 var commits = $el.data('toggleCommitsCnt');
1343 1343 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1344 1344 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1345 1345
1346 1346 if ($el.hasClass('collapsed')) {
1347 1347 $('.compare_select').show();
1348 1348 $('.compare_select_hidden').hide();
1349 1349
1350 1350 $el.removeClass('collapsed');
1351 1351 $el.html(
1352 1352 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1353 1353 collapseMsg);
1354 1354 }
1355 1355 else {
1356 1356 $('.compare_select').hide();
1357 1357 $('.compare_select_hidden').show();
1358 1358 $el.addClass('collapsed');
1359 1359 $el.html(
1360 1360 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1361 1361 expandMsg);
1362 1362 }
1363 1363 updateSticky();
1364 1364 };
1365 1365
1366 1366 // get stored diff mode and pre-enable it
1367 1367 if (templateContext.session_attrs.wide_diff_mode === "true") {
1368 1368 Rhodecode.comments.toggleWideMode(null);
1369 1369 $('.toggle-wide-diff').addClass('btn-active');
1370 1370 updateSticky();
1371 1371 }
1372 1372
1373 1373 // DIFF NAV //
1374 1374
1375 1375 // element to detect scroll direction of
1376 1376 var $window = $(window);
1377 1377
1378 1378 // initialize last scroll position
1379 1379 var lastScrollY = $window.scrollTop();
1380 1380
1381 1381 $window.on('resize scrollstop', {latency: 350}, function () {
1382 1382 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1383 1383
1384 1384 // get current scroll position
1385 1385 var currentScrollY = $window.scrollTop();
1386 1386
1387 1387 // determine current scroll direction
1388 1388 if (currentScrollY > lastScrollY) {
1389 1389 var y = 'down'
1390 1390 } else if (currentScrollY !== lastScrollY) {
1391 1391 var y = 'up';
1392 1392 }
1393 1393
1394 1394 var pos = -1; // by default we use last element in viewport
1395 1395 if (y === 'down') {
1396 1396 pos = -1;
1397 1397 } else if (y === 'up') {
1398 1398 pos = 0;
1399 1399 }
1400 1400
1401 1401 if (visibleChunks.length > 0) {
1402 1402 $('.nav-chunk').removeClass('selected');
1403 1403 $(visibleChunks.get(pos)).addClass('selected');
1404 1404 }
1405 1405
1406 1406 // update last scroll position to current position
1407 1407 lastScrollY = currentScrollY;
1408 1408
1409 1409 });
1410 1410 $('#diff_nav').html(diffNavText);
1411 1411
1412 1412 });
1413 1413 </script>
1414 1414
1415 1415 </%def>
General Comments 0
You need to be logged in to leave comments. Login now