##// END OF EJS Templates
pull-requests: add indication of state change in list of pull-requests and actually show them in the list.
dan -
r3816:18ef8156 stable
parent child Browse files
Show More
@@ -1,743 +1,743 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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
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 LoginRequired, NotAnonymous, CSRFRequired, \
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, joinedload,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.scm import RepoList
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.user_group import UserGroupModel
53 53 from rhodecode.model.validation_schema.schemas import user_schema
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MyAccountView(BaseAppView, DataGridAppView):
59 59 ALLOW_SCOPED_TOKENS = False
60 60 """
61 61 This view has alternative version inside EE, if modified please take a look
62 62 in there as well.
63 63 """
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.user = c.auth_user.get_instance()
68 68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
69 69
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 @view_config(
75 75 route_name='my_account_profile', request_method='GET',
76 76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
77 77 def my_account_profile(self):
78 78 c = self.load_default_context()
79 79 c.active = 'profile'
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 def maybe_attach_token_scope(self, token):
168 168 # implemented in EE edition
169 169 pass
170 170
171 171 @LoginRequired()
172 172 @NotAnonymous()
173 173 @CSRFRequired()
174 174 @view_config(
175 175 route_name='my_account_auth_tokens_add', request_method='POST',)
176 176 def my_account_auth_tokens_add(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179
180 180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
181 181 description = self.request.POST.get('description')
182 182 role = self.request.POST.get('role')
183 183
184 184 token = UserModel().add_auth_token(
185 185 user=c.user.user_id,
186 186 lifetime_minutes=lifetime, role=role, description=description,
187 187 scope_callback=self.maybe_attach_token_scope)
188 188 token_data = token.get_api_data()
189 189
190 190 audit_logger.store_web(
191 191 'user.edit.token.add', action_data={
192 192 'data': {'token': token_data, 'user': 'self'}},
193 193 user=self._rhodecode_user, )
194 194 Session().commit()
195 195
196 196 h.flash(_("Auth token successfully created"), category='success')
197 197 return HTTPFound(h.route_path('my_account_auth_tokens'))
198 198
199 199 @LoginRequired()
200 200 @NotAnonymous()
201 201 @CSRFRequired()
202 202 @view_config(
203 203 route_name='my_account_auth_tokens_delete', request_method='POST')
204 204 def my_account_auth_tokens_delete(self):
205 205 _ = self.request.translate
206 206 c = self.load_default_context()
207 207
208 208 del_auth_token = self.request.POST.get('del_auth_token')
209 209
210 210 if del_auth_token:
211 211 token = UserApiKeys.get_or_404(del_auth_token)
212 212 token_data = token.get_api_data()
213 213
214 214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
215 215 audit_logger.store_web(
216 216 'user.edit.token.delete', action_data={
217 217 'data': {'token': token_data, 'user': 'self'}},
218 218 user=self._rhodecode_user,)
219 219 Session().commit()
220 220 h.flash(_("Auth token successfully deleted"), category='success')
221 221
222 222 return HTTPFound(h.route_path('my_account_auth_tokens'))
223 223
224 224 @LoginRequired()
225 225 @NotAnonymous()
226 226 @view_config(
227 227 route_name='my_account_emails', request_method='GET',
228 228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 229 def my_account_emails(self):
230 230 _ = self.request.translate
231 231
232 232 c = self.load_default_context()
233 233 c.active = 'emails'
234 234
235 235 c.user_email_map = UserEmailMap.query()\
236 236 .filter(UserEmailMap.user == c.user).all()
237 237
238 238 schema = user_schema.AddEmailSchema().bind(
239 239 username=c.user.username, user_emails=c.user.emails)
240 240
241 241 form = forms.RcForm(schema,
242 242 action=h.route_path('my_account_emails_add'),
243 243 buttons=(forms.buttons.save, forms.buttons.reset))
244 244
245 245 c.form = form
246 246 return self._get_template_context(c)
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 @CSRFRequired()
251 251 @view_config(
252 252 route_name='my_account_emails_add', request_method='POST',
253 253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
254 254 def my_account_emails_add(self):
255 255 _ = self.request.translate
256 256 c = self.load_default_context()
257 257 c.active = 'emails'
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(
263 263 schema, action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 controls = self.request.POST.items()
267 267 try:
268 268 valid_data = form.validate(controls)
269 269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
270 270 audit_logger.store_web(
271 271 'user.edit.email.add', action_data={
272 272 'data': {'email': valid_data['email'], 'user': 'self'}},
273 273 user=self._rhodecode_user,)
274 274 Session().commit()
275 275 except formencode.Invalid as error:
276 276 h.flash(h.escape(error.error_dict['email']), category='error')
277 277 except forms.ValidationFailure as e:
278 278 c.user_email_map = UserEmailMap.query() \
279 279 .filter(UserEmailMap.user == c.user).all()
280 280 c.form = e
281 281 return self._get_template_context(c)
282 282 except Exception:
283 283 log.exception("Exception adding email")
284 284 h.flash(_('Error occurred during adding email'),
285 285 category='error')
286 286 else:
287 287 h.flash(_("Successfully added email"), category='success')
288 288
289 289 raise HTTPFound(self.request.route_path('my_account_emails'))
290 290
291 291 @LoginRequired()
292 292 @NotAnonymous()
293 293 @CSRFRequired()
294 294 @view_config(
295 295 route_name='my_account_emails_delete', request_method='POST')
296 296 def my_account_emails_delete(self):
297 297 _ = self.request.translate
298 298 c = self.load_default_context()
299 299
300 300 del_email_id = self.request.POST.get('del_email_id')
301 301 if del_email_id:
302 302 email = UserEmailMap.get_or_404(del_email_id).email
303 303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
304 304 audit_logger.store_web(
305 305 'user.edit.email.delete', action_data={
306 306 'data': {'email': email, 'user': 'self'}},
307 307 user=self._rhodecode_user,)
308 308 Session().commit()
309 309 h.flash(_("Email successfully deleted"),
310 310 category='success')
311 311 return HTTPFound(h.route_path('my_account_emails'))
312 312
313 313 @LoginRequired()
314 314 @NotAnonymous()
315 315 @CSRFRequired()
316 316 @view_config(
317 317 route_name='my_account_notifications_test_channelstream',
318 318 request_method='POST', renderer='json_ext')
319 319 def my_account_notifications_test_channelstream(self):
320 320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
321 321 self._rhodecode_user.username, datetime.datetime.now())
322 322 payload = {
323 323 # 'channel': 'broadcast',
324 324 'type': 'message',
325 325 'timestamp': datetime.datetime.utcnow(),
326 326 'user': 'system',
327 327 'pm_users': [self._rhodecode_user.username],
328 328 'message': {
329 329 'message': message,
330 330 'level': 'info',
331 331 'topic': '/notifications'
332 332 }
333 333 }
334 334
335 335 registry = self.request.registry
336 336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
337 337 channelstream_config = rhodecode_plugins.get('channelstream', {})
338 338
339 339 try:
340 340 channelstream_request(channelstream_config, [payload], '/message')
341 341 except ChannelstreamException as e:
342 342 log.exception('Failed to send channelstream data')
343 343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
344 344 return {"response": 'Channelstream data sent. '
345 345 'You should see a new live message now.'}
346 346
347 347 def _load_my_repos_data(self, watched=False):
348 348 if watched:
349 349 admin = False
350 350 follows_repos = Session().query(UserFollowing)\
351 351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
352 352 .options(joinedload(UserFollowing.follows_repository))\
353 353 .all()
354 354 repo_list = [x.follows_repository for x in follows_repos]
355 355 else:
356 356 admin = True
357 357 repo_list = Repository.get_all_repos(
358 358 user_id=self._rhodecode_user.user_id)
359 359 repo_list = RepoList(repo_list, perm_set=[
360 360 'repository.read', 'repository.write', 'repository.admin'])
361 361
362 362 repos_data = RepoModel().get_repos_as_dict(
363 363 repo_list=repo_list, admin=admin, short_name=False)
364 364 # json used to render the grid
365 365 return json.dumps(repos_data)
366 366
367 367 @LoginRequired()
368 368 @NotAnonymous()
369 369 @view_config(
370 370 route_name='my_account_repos', request_method='GET',
371 371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 372 def my_account_repos(self):
373 373 c = self.load_default_context()
374 374 c.active = 'repos'
375 375
376 376 # json used to render the grid
377 377 c.data = self._load_my_repos_data()
378 378 return self._get_template_context(c)
379 379
380 380 @LoginRequired()
381 381 @NotAnonymous()
382 382 @view_config(
383 383 route_name='my_account_watched', request_method='GET',
384 384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
385 385 def my_account_watched(self):
386 386 c = self.load_default_context()
387 387 c.active = 'watched'
388 388
389 389 # json used to render the grid
390 390 c.data = self._load_my_repos_data(watched=True)
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @NotAnonymous()
395 395 @view_config(
396 396 route_name='my_account_bookmarks', request_method='GET',
397 397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 398 def my_account_bookmarks(self):
399 399 c = self.load_default_context()
400 400 c.active = 'bookmarks'
401 401 return self._get_template_context(c)
402 402
403 403 def _process_entry(self, entry, user_id):
404 404 position = safe_int(entry.get('position'))
405 405 if position is None:
406 406 return
407 407
408 408 # check if this is an existing entry
409 409 is_new = False
410 410 db_entry = UserBookmark().get_by_position_for_user(position, user_id)
411 411
412 412 if db_entry and str2bool(entry.get('remove')):
413 413 log.debug('Marked bookmark %s for deletion', db_entry)
414 414 Session().delete(db_entry)
415 415 return
416 416
417 417 if not db_entry:
418 418 # new
419 419 db_entry = UserBookmark()
420 420 is_new = True
421 421
422 422 should_save = False
423 423 default_redirect_url = ''
424 424
425 425 # save repo
426 426 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
427 427 repo = Repository.get(entry['bookmark_repo'])
428 428 perm_check = HasRepoPermissionAny(
429 429 'repository.read', 'repository.write', 'repository.admin')
430 430 if repo and perm_check(repo_name=repo.repo_name):
431 431 db_entry.repository = repo
432 432 should_save = True
433 433 default_redirect_url = '${repo_url}'
434 434 # save repo group
435 435 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
436 436 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
437 437 perm_check = HasRepoGroupPermissionAny(
438 438 'group.read', 'group.write', 'group.admin')
439 439
440 440 if repo_group and perm_check(group_name=repo_group.group_name):
441 441 db_entry.repository_group = repo_group
442 442 should_save = True
443 443 default_redirect_url = '${repo_group_url}'
444 444 # save generic info
445 445 elif entry.get('title') and entry.get('redirect_url'):
446 446 should_save = True
447 447
448 448 if should_save:
449 449 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
450 450 # mark user and position
451 451 db_entry.user_id = user_id
452 452 db_entry.position = position
453 453 db_entry.title = entry.get('title')
454 454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455 455
456 456 Session().add(db_entry)
457 457
458 458 @LoginRequired()
459 459 @NotAnonymous()
460 460 @CSRFRequired()
461 461 @view_config(
462 462 route_name='my_account_bookmarks_update', request_method='POST')
463 463 def my_account_bookmarks_update(self):
464 464 _ = self.request.translate
465 465 c = self.load_default_context()
466 466 c.active = 'bookmarks'
467 467
468 468 controls = peppercorn.parse(self.request.POST.items())
469 469 user_id = c.user.user_id
470 470
471 471 try:
472 472 for entry in controls.get('bookmarks', []):
473 473 self._process_entry(entry, user_id)
474 474
475 475 Session().commit()
476 476 h.flash(_("Update Bookmarks"), category='success')
477 477 except IntegrityError:
478 478 h.flash(_("Failed to update bookmarks. "
479 479 "Make sure an unique position is used"), category='error')
480 480
481 481 return HTTPFound(h.route_path('my_account_bookmarks'))
482 482
483 483 @LoginRequired()
484 484 @NotAnonymous()
485 485 @view_config(
486 486 route_name='my_account_goto_bookmark', request_method='GET',
487 487 renderer='rhodecode:templates/admin/my_account/my_account.mako')
488 488 def my_account_goto_bookmark(self):
489 489
490 490 bookmark_id = self.request.matchdict['bookmark_id']
491 491 user_bookmark = UserBookmark().query()\
492 492 .filter(UserBookmark.user_id == self.request.user.user_id) \
493 493 .filter(UserBookmark.position == bookmark_id).scalar()
494 494
495 495 redirect_url = h.route_path('my_account_bookmarks')
496 496 if not user_bookmark:
497 497 raise HTTPFound(redirect_url)
498 498
499 499 # repository set
500 500 if user_bookmark.repository:
501 501 repo_name = user_bookmark.repository.repo_name
502 502 base_redirect_url = h.route_path(
503 503 'repo_summary', repo_name=repo_name)
504 504 if user_bookmark.redirect_url and \
505 505 '${repo_url}' in user_bookmark.redirect_url:
506 506 redirect_url = string.Template(user_bookmark.redirect_url)\
507 507 .safe_substitute({'repo_url': base_redirect_url})
508 508 else:
509 509 redirect_url = base_redirect_url
510 510 # repository group set
511 511 elif user_bookmark.repository_group:
512 512 repo_group_name = user_bookmark.repository_group.group_name
513 513 base_redirect_url = h.route_path(
514 514 'repo_group_home', repo_group_name=repo_group_name)
515 515 if user_bookmark.redirect_url and \
516 516 '${repo_group_url}' in user_bookmark.redirect_url:
517 517 redirect_url = string.Template(user_bookmark.redirect_url)\
518 518 .safe_substitute({'repo_group_url': base_redirect_url})
519 519 else:
520 520 redirect_url = base_redirect_url
521 521 # custom URL set
522 522 elif user_bookmark.redirect_url:
523 523 server_url = h.route_url('home').rstrip('/')
524 524 redirect_url = string.Template(user_bookmark.redirect_url) \
525 525 .safe_substitute({'server_url': server_url})
526 526
527 527 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
528 528 raise HTTPFound(redirect_url)
529 529
530 530 @LoginRequired()
531 531 @NotAnonymous()
532 532 @view_config(
533 533 route_name='my_account_perms', request_method='GET',
534 534 renderer='rhodecode:templates/admin/my_account/my_account.mako')
535 535 def my_account_perms(self):
536 536 c = self.load_default_context()
537 537 c.active = 'perms'
538 538
539 539 c.perm_user = c.auth_user
540 540 return self._get_template_context(c)
541 541
542 542 @LoginRequired()
543 543 @NotAnonymous()
544 544 @view_config(
545 545 route_name='my_account_notifications', request_method='GET',
546 546 renderer='rhodecode:templates/admin/my_account/my_account.mako')
547 547 def my_notifications(self):
548 548 c = self.load_default_context()
549 549 c.active = 'notifications'
550 550
551 551 return self._get_template_context(c)
552 552
553 553 @LoginRequired()
554 554 @NotAnonymous()
555 555 @CSRFRequired()
556 556 @view_config(
557 557 route_name='my_account_notifications_toggle_visibility',
558 558 request_method='POST', renderer='json_ext')
559 559 def my_notifications_toggle_visibility(self):
560 560 user = self._rhodecode_db_user
561 561 new_status = not user.user_data.get('notification_status', True)
562 562 user.update_userdata(notification_status=new_status)
563 563 Session().commit()
564 564 return user.user_data['notification_status']
565 565
566 566 @LoginRequired()
567 567 @NotAnonymous()
568 568 @view_config(
569 569 route_name='my_account_edit',
570 570 request_method='GET',
571 571 renderer='rhodecode:templates/admin/my_account/my_account.mako')
572 572 def my_account_edit(self):
573 573 c = self.load_default_context()
574 574 c.active = 'profile_edit'
575 575 c.extern_type = c.user.extern_type
576 576 c.extern_name = c.user.extern_name
577 577
578 578 schema = user_schema.UserProfileSchema().bind(
579 579 username=c.user.username, user_emails=c.user.emails)
580 580 appstruct = {
581 581 'username': c.user.username,
582 582 'email': c.user.email,
583 583 'firstname': c.user.firstname,
584 584 'lastname': c.user.lastname,
585 585 }
586 586 c.form = forms.RcForm(
587 587 schema, appstruct=appstruct,
588 588 action=h.route_path('my_account_update'),
589 589 buttons=(forms.buttons.save, forms.buttons.reset))
590 590
591 591 return self._get_template_context(c)
592 592
593 593 @LoginRequired()
594 594 @NotAnonymous()
595 595 @CSRFRequired()
596 596 @view_config(
597 597 route_name='my_account_update',
598 598 request_method='POST',
599 599 renderer='rhodecode:templates/admin/my_account/my_account.mako')
600 600 def my_account_update(self):
601 601 _ = self.request.translate
602 602 c = self.load_default_context()
603 603 c.active = 'profile_edit'
604 604 c.perm_user = c.auth_user
605 605 c.extern_type = c.user.extern_type
606 606 c.extern_name = c.user.extern_name
607 607
608 608 schema = user_schema.UserProfileSchema().bind(
609 609 username=c.user.username, user_emails=c.user.emails)
610 610 form = forms.RcForm(
611 611 schema, buttons=(forms.buttons.save, forms.buttons.reset))
612 612
613 613 controls = self.request.POST.items()
614 614 try:
615 615 valid_data = form.validate(controls)
616 616 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
617 617 'new_password', 'password_confirmation']
618 618 if c.extern_type != "rhodecode":
619 619 # forbid updating username for external accounts
620 620 skip_attrs.append('username')
621 621 old_email = c.user.email
622 622 UserModel().update_user(
623 623 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
624 624 **valid_data)
625 625 if old_email != valid_data['email']:
626 626 old = UserEmailMap.query() \
627 627 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
628 628 old.email = old_email
629 629 h.flash(_('Your account was updated successfully'), category='success')
630 630 Session().commit()
631 631 except forms.ValidationFailure as e:
632 632 c.form = e
633 633 return self._get_template_context(c)
634 634 except Exception:
635 635 log.exception("Exception updating user")
636 636 h.flash(_('Error occurred during update of user'),
637 637 category='error')
638 638 raise HTTPFound(h.route_path('my_account_profile'))
639 639
640 640 def _get_pull_requests_list(self, statuses):
641 641 draw, start, limit = self._extract_chunk(self.request)
642 642 search_q, order_by, order_dir = self._extract_ordering(self.request)
643 643 _render = self.request.get_partial_renderer(
644 644 'rhodecode:templates/data_table/_dt_elements.mako')
645 645
646 646 pull_requests = PullRequestModel().get_im_participating_in(
647 647 user_id=self._rhodecode_user.user_id,
648 648 statuses=statuses,
649 649 offset=start, length=limit, order_by=order_by,
650 650 order_dir=order_dir)
651 651
652 652 pull_requests_total_count = PullRequestModel().count_im_participating_in(
653 653 user_id=self._rhodecode_user.user_id, statuses=statuses)
654 654
655 655 data = []
656 656 comments_model = CommentsModel()
657 657 for pr in pull_requests:
658 658 repo_id = pr.target_repo_id
659 659 comments = comments_model.get_all_comments(
660 660 repo_id, pull_request=pr)
661 661 owned = pr.user_id == self._rhodecode_user.user_id
662 662
663 663 data.append({
664 664 'target_repo': _render('pullrequest_target_repo',
665 665 pr.target_repo.repo_name),
666 666 'name': _render('pullrequest_name',
667 667 pr.pull_request_id, pr.target_repo.repo_name,
668 668 short=True),
669 669 'name_raw': pr.pull_request_id,
670 670 'status': _render('pullrequest_status',
671 671 pr.calculated_review_status()),
672 'title': _render(
673 'pullrequest_title', pr.title, pr.description),
672 'title': _render('pullrequest_title', pr.title, pr.description),
674 673 'description': h.escape(pr.description),
675 674 'updated_on': _render('pullrequest_updated_on',
676 675 h.datetime_to_time(pr.updated_on)),
677 676 'updated_on_raw': h.datetime_to_time(pr.updated_on),
678 677 'created_on': _render('pullrequest_updated_on',
679 678 h.datetime_to_time(pr.created_on)),
680 679 'created_on_raw': h.datetime_to_time(pr.created_on),
680 'state': pr.pull_request_state,
681 681 'author': _render('pullrequest_author',
682 682 pr.author.full_contact, ),
683 683 'author_raw': pr.author.full_name,
684 684 'comments': _render('pullrequest_comments', len(comments)),
685 685 'comments_raw': len(comments),
686 686 'closed': pr.is_closed(),
687 687 'owned': owned
688 688 })
689 689
690 690 # json used to render the grid
691 691 data = ({
692 692 'draw': draw,
693 693 'data': data,
694 694 'recordsTotal': pull_requests_total_count,
695 695 'recordsFiltered': pull_requests_total_count,
696 696 })
697 697 return data
698 698
699 699 @LoginRequired()
700 700 @NotAnonymous()
701 701 @view_config(
702 702 route_name='my_account_pullrequests',
703 703 request_method='GET',
704 704 renderer='rhodecode:templates/admin/my_account/my_account.mako')
705 705 def my_account_pullrequests(self):
706 706 c = self.load_default_context()
707 707 c.active = 'pullrequests'
708 708 req_get = self.request.GET
709 709
710 710 c.closed = str2bool(req_get.get('pr_show_closed'))
711 711
712 712 return self._get_template_context(c)
713 713
714 714 @LoginRequired()
715 715 @NotAnonymous()
716 716 @view_config(
717 717 route_name='my_account_pullrequests_data',
718 718 request_method='GET', renderer='json_ext')
719 719 def my_account_pullrequests_data(self):
720 720 self.load_default_context()
721 721 req_get = self.request.GET
722 722 closed = str2bool(req_get.get('closed'))
723 723
724 724 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
725 725 if closed:
726 726 statuses += [PullRequest.STATUS_CLOSED]
727 727
728 728 data = self._get_pull_requests_list(statuses=statuses)
729 729 return data
730 730
731 731 @LoginRequired()
732 732 @NotAnonymous()
733 733 @view_config(
734 734 route_name='my_account_user_group_membership',
735 735 request_method='GET',
736 736 renderer='rhodecode:templates/admin/my_account/my_account.mako')
737 737 def my_account_user_group_membership(self):
738 738 c = self.load_default_context()
739 739 c.active = 'user_group_membership'
740 740 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
741 741 for group in self._rhodecode_db_user.group_member]
742 742 c.user_groups = json.dumps(groups)
743 743 return self._get_template_context(c)
@@ -1,1464 +1,1464 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.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.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 # backward compat., we use for OLD PRs a plain renderer
64 64 c.renderer = 'plain'
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 'title': _render(
116 'pullrequest_title', pr.title, pr.description),
115 'title': _render('pullrequest_title', pr.title, pr.description),
117 116 'description': h.escape(pr.description),
118 117 'updated_on': _render('pullrequest_updated_on',
119 118 h.datetime_to_time(pr.updated_on)),
120 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 120 'created_on': _render('pullrequest_updated_on',
122 121 h.datetime_to_time(pr.created_on)),
123 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'state': pr.pull_request_state,
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff, hide_whitespace_changes, diff_context):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id,
218 218 hide_whitespace_changes, diff_context)
219 219
220 220 diff_processor = diffs.DiffProcessor(
221 221 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 222 file_limit=file_limit, show_full_diff=fulldiff)
223 223
224 224 _parsed = diff_processor.prepare()
225 225
226 226 diffset = codeblocks.DiffSet(
227 227 repo_name=self.db_repo_name,
228 228 source_repo_name=source_repo_name,
229 229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 231 )
232 232 diffset = self.path_filter.render_patchset_filtered(
233 233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 234
235 235 return diffset
236 236
237 237 def _get_range_diffset(self, source_scm, source_repo,
238 238 commit1, commit2, diff_limit, file_limit,
239 239 fulldiff, hide_whitespace_changes, diff_context):
240 240 vcs_diff = source_scm.get_diff(
241 241 commit1, commit2,
242 242 ignore_whitespace=hide_whitespace_changes,
243 243 context=diff_context)
244 244
245 245 diff_processor = diffs.DiffProcessor(
246 246 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 247 file_limit=file_limit, show_full_diff=fulldiff)
248 248
249 249 _parsed = diff_processor.prepare()
250 250
251 251 diffset = codeblocks.DiffSet(
252 252 repo_name=source_repo.repo_name,
253 253 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 254 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 255
256 256 diffset = self.path_filter.render_patchset_filtered(
257 257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 _ = self.request.translate
269 269 c = self.load_default_context()
270 270
271 271 pull_request = PullRequest.get_or_404(
272 272 self.request.matchdict['pull_request_id'])
273 273 pull_request_id = pull_request.pull_request_id
274 274
275 275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 276 log.debug('show: forbidden because pull request is in state %s',
277 277 pull_request.pull_request_state)
278 278 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 280 pull_request.pull_request_state)
281 281 h.flash(msg, category='error')
282 282 raise HTTPFound(h.route_path('pullrequest_show_all',
283 283 repo_name=self.db_repo_name))
284 284
285 285 version = self.request.GET.get('version')
286 286 from_version = self.request.GET.get('from_version') or version
287 287 merge_checks = self.request.GET.get('merge_checks')
288 288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
289 289
290 290 # fetch global flags of ignore ws or context lines
291 291 diff_context = diffs.get_diff_context(self.request)
292 292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
293 293
294 294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
295 295
296 296 (pull_request_latest,
297 297 pull_request_at_ver,
298 298 pull_request_display_obj,
299 299 at_version) = PullRequestModel().get_pr_version(
300 300 pull_request_id, version=version)
301 301 pr_closed = pull_request_latest.is_closed()
302 302
303 303 if pr_closed and (version or from_version):
304 304 # not allow to browse versions
305 305 raise HTTPFound(h.route_path(
306 306 'pullrequest_show', repo_name=self.db_repo_name,
307 307 pull_request_id=pull_request_id))
308 308
309 309 versions = pull_request_display_obj.versions()
310 310 # used to store per-commit range diffs
311 311 c.changes = collections.OrderedDict()
312 312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
313 313
314 314 c.at_version = at_version
315 315 c.at_version_num = (at_version
316 316 if at_version and at_version != 'latest'
317 317 else None)
318 318 c.at_version_pos = ChangesetComment.get_index_from_version(
319 319 c.at_version_num, versions)
320 320
321 321 (prev_pull_request_latest,
322 322 prev_pull_request_at_ver,
323 323 prev_pull_request_display_obj,
324 324 prev_at_version) = PullRequestModel().get_pr_version(
325 325 pull_request_id, version=from_version)
326 326
327 327 c.from_version = prev_at_version
328 328 c.from_version_num = (prev_at_version
329 329 if prev_at_version and prev_at_version != 'latest'
330 330 else None)
331 331 c.from_version_pos = ChangesetComment.get_index_from_version(
332 332 c.from_version_num, versions)
333 333
334 334 # define if we're in COMPARE mode or VIEW at version mode
335 335 compare = at_version != prev_at_version
336 336
337 337 # pull_requests repo_name we opened it against
338 338 # ie. target_repo must match
339 339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
340 340 raise HTTPNotFound()
341 341
342 342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
343 343 pull_request_at_ver)
344 344
345 345 c.pull_request = pull_request_display_obj
346 346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
347 347 c.pull_request_latest = pull_request_latest
348 348
349 349 if compare or (at_version and not at_version == 'latest'):
350 350 c.allowed_to_change_status = False
351 351 c.allowed_to_update = False
352 352 c.allowed_to_merge = False
353 353 c.allowed_to_delete = False
354 354 c.allowed_to_comment = False
355 355 c.allowed_to_close = False
356 356 else:
357 357 can_change_status = PullRequestModel().check_user_change_status(
358 358 pull_request_at_ver, self._rhodecode_user)
359 359 c.allowed_to_change_status = can_change_status and not pr_closed
360 360
361 361 c.allowed_to_update = PullRequestModel().check_user_update(
362 362 pull_request_latest, self._rhodecode_user) and not pr_closed
363 363 c.allowed_to_merge = PullRequestModel().check_user_merge(
364 364 pull_request_latest, self._rhodecode_user) and not pr_closed
365 365 c.allowed_to_delete = PullRequestModel().check_user_delete(
366 366 pull_request_latest, self._rhodecode_user) and not pr_closed
367 367 c.allowed_to_comment = not pr_closed
368 368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
369 369
370 370 c.forbid_adding_reviewers = False
371 371 c.forbid_author_to_review = False
372 372 c.forbid_commit_author_to_review = False
373 373
374 374 if pull_request_latest.reviewer_data and \
375 375 'rules' in pull_request_latest.reviewer_data:
376 376 rules = pull_request_latest.reviewer_data['rules'] or {}
377 377 try:
378 378 c.forbid_adding_reviewers = rules.get(
379 379 'forbid_adding_reviewers')
380 380 c.forbid_author_to_review = rules.get(
381 381 'forbid_author_to_review')
382 382 c.forbid_commit_author_to_review = rules.get(
383 383 'forbid_commit_author_to_review')
384 384 except Exception:
385 385 pass
386 386
387 387 # check merge capabilities
388 388 _merge_check = MergeCheck.validate(
389 389 pull_request_latest, auth_user=self._rhodecode_user,
390 390 translator=self.request.translate,
391 391 force_shadow_repo_refresh=force_refresh)
392 392 c.pr_merge_errors = _merge_check.error_details
393 393 c.pr_merge_possible = not _merge_check.failed
394 394 c.pr_merge_message = _merge_check.merge_msg
395 395
396 396 c.pr_merge_info = MergeCheck.get_merge_conditions(
397 397 pull_request_latest, translator=self.request.translate)
398 398
399 399 c.pull_request_review_status = _merge_check.review_status
400 400 if merge_checks:
401 401 self.request.override_renderer = \
402 402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
403 403 return self._get_template_context(c)
404 404
405 405 comments_model = CommentsModel()
406 406
407 407 # reviewers and statuses
408 408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
409 409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
410 410
411 411 # GENERAL COMMENTS with versions #
412 412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
413 413 q = q.order_by(ChangesetComment.comment_id.asc())
414 414 general_comments = q
415 415
416 416 # pick comments we want to render at current version
417 417 c.comment_versions = comments_model.aggregate_comments(
418 418 general_comments, versions, c.at_version_num)
419 419 c.comments = c.comment_versions[c.at_version_num]['until']
420 420
421 421 # INLINE COMMENTS with versions #
422 422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
423 423 q = q.order_by(ChangesetComment.comment_id.asc())
424 424 inline_comments = q
425 425
426 426 c.inline_versions = comments_model.aggregate_comments(
427 427 inline_comments, versions, c.at_version_num, inline=True)
428 428
429 429 # inject latest version
430 430 latest_ver = PullRequest.get_pr_display_object(
431 431 pull_request_latest, pull_request_latest)
432 432
433 433 c.versions = versions + [latest_ver]
434 434
435 435 # if we use version, then do not show later comments
436 436 # than current version
437 437 display_inline_comments = collections.defaultdict(
438 438 lambda: collections.defaultdict(list))
439 439 for co in inline_comments:
440 440 if c.at_version_num:
441 441 # pick comments that are at least UPTO given version, so we
442 442 # don't render comments for higher version
443 443 should_render = co.pull_request_version_id and \
444 444 co.pull_request_version_id <= c.at_version_num
445 445 else:
446 446 # showing all, for 'latest'
447 447 should_render = True
448 448
449 449 if should_render:
450 450 display_inline_comments[co.f_path][co.line_no].append(co)
451 451
452 452 # load diff data into template context, if we use compare mode then
453 453 # diff is calculated based on changes between versions of PR
454 454
455 455 source_repo = pull_request_at_ver.source_repo
456 456 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
457 457
458 458 target_repo = pull_request_at_ver.target_repo
459 459 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
460 460
461 461 if compare:
462 462 # in compare switch the diff base to latest commit from prev version
463 463 target_ref_id = prev_pull_request_display_obj.revisions[0]
464 464
465 465 # despite opening commits for bookmarks/branches/tags, we always
466 466 # convert this to rev to prevent changes after bookmark or branch change
467 467 c.source_ref_type = 'rev'
468 468 c.source_ref = source_ref_id
469 469
470 470 c.target_ref_type = 'rev'
471 471 c.target_ref = target_ref_id
472 472
473 473 c.source_repo = source_repo
474 474 c.target_repo = target_repo
475 475
476 476 c.commit_ranges = []
477 477 source_commit = EmptyCommit()
478 478 target_commit = EmptyCommit()
479 479 c.missing_requirements = False
480 480
481 481 source_scm = source_repo.scm_instance()
482 482 target_scm = target_repo.scm_instance()
483 483
484 484 shadow_scm = None
485 485 try:
486 486 shadow_scm = pull_request_latest.get_shadow_repo()
487 487 except Exception:
488 488 log.debug('Failed to get shadow repo', exc_info=True)
489 489 # try first the existing source_repo, and then shadow
490 490 # repo if we can obtain one
491 491 commits_source_repo = source_scm or shadow_scm
492 492
493 493 c.commits_source_repo = commits_source_repo
494 494 c.ancestor = None # set it to None, to hide it from PR view
495 495
496 496 # empty version means latest, so we keep this to prevent
497 497 # double caching
498 498 version_normalized = version or 'latest'
499 499 from_version_normalized = from_version or 'latest'
500 500
501 501 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
502 502 cache_file_path = diff_cache_exist(
503 503 cache_path, 'pull_request', pull_request_id, version_normalized,
504 504 from_version_normalized, source_ref_id, target_ref_id,
505 505 hide_whitespace_changes, diff_context, c.fulldiff)
506 506
507 507 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
508 508 force_recache = self.get_recache_flag()
509 509
510 510 cached_diff = None
511 511 if caching_enabled:
512 512 cached_diff = load_cached_diff(cache_file_path)
513 513
514 514 has_proper_commit_cache = (
515 515 cached_diff and cached_diff.get('commits')
516 516 and len(cached_diff.get('commits', [])) == 5
517 517 and cached_diff.get('commits')[0]
518 518 and cached_diff.get('commits')[3])
519 519
520 520 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
521 521 diff_commit_cache = \
522 522 (ancestor_commit, commit_cache, missing_requirements,
523 523 source_commit, target_commit) = cached_diff['commits']
524 524 else:
525 525 diff_commit_cache = \
526 526 (ancestor_commit, commit_cache, missing_requirements,
527 527 source_commit, target_commit) = self.get_commits(
528 528 commits_source_repo,
529 529 pull_request_at_ver,
530 530 source_commit,
531 531 source_ref_id,
532 532 source_scm,
533 533 target_commit,
534 534 target_ref_id,
535 535 target_scm)
536 536
537 537 # register our commit range
538 538 for comm in commit_cache.values():
539 539 c.commit_ranges.append(comm)
540 540
541 541 c.missing_requirements = missing_requirements
542 542 c.ancestor_commit = ancestor_commit
543 543 c.statuses = source_repo.statuses(
544 544 [x.raw_id for x in c.commit_ranges])
545 545
546 546 # auto collapse if we have more than limit
547 547 collapse_limit = diffs.DiffProcessor._collapse_commits_over
548 548 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
549 549 c.compare_mode = compare
550 550
551 551 # diff_limit is the old behavior, will cut off the whole diff
552 552 # if the limit is applied otherwise will just hide the
553 553 # big files from the front-end
554 554 diff_limit = c.visual.cut_off_limit_diff
555 555 file_limit = c.visual.cut_off_limit_file
556 556
557 557 c.missing_commits = False
558 558 if (c.missing_requirements
559 559 or isinstance(source_commit, EmptyCommit)
560 560 or source_commit == target_commit):
561 561
562 562 c.missing_commits = True
563 563 else:
564 564 c.inline_comments = display_inline_comments
565 565
566 566 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
567 567 if not force_recache and has_proper_diff_cache:
568 568 c.diffset = cached_diff['diff']
569 569 (ancestor_commit, commit_cache, missing_requirements,
570 570 source_commit, target_commit) = cached_diff['commits']
571 571 else:
572 572 c.diffset = self._get_diffset(
573 573 c.source_repo.repo_name, commits_source_repo,
574 574 source_ref_id, target_ref_id,
575 575 target_commit, source_commit,
576 576 diff_limit, file_limit, c.fulldiff,
577 577 hide_whitespace_changes, diff_context)
578 578
579 579 # save cached diff
580 580 if caching_enabled:
581 581 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
582 582
583 583 c.limited_diff = c.diffset.limited_diff
584 584
585 585 # calculate removed files that are bound to comments
586 586 comment_deleted_files = [
587 587 fname for fname in display_inline_comments
588 588 if fname not in c.diffset.file_stats]
589 589
590 590 c.deleted_files_comments = collections.defaultdict(dict)
591 591 for fname, per_line_comments in display_inline_comments.items():
592 592 if fname in comment_deleted_files:
593 593 c.deleted_files_comments[fname]['stats'] = 0
594 594 c.deleted_files_comments[fname]['comments'] = list()
595 595 for lno, comments in per_line_comments.items():
596 596 c.deleted_files_comments[fname]['comments'].extend(comments)
597 597
598 598 # maybe calculate the range diff
599 599 if c.range_diff_on:
600 600 # TODO(marcink): set whitespace/context
601 601 context_lcl = 3
602 602 ign_whitespace_lcl = False
603 603
604 604 for commit in c.commit_ranges:
605 605 commit2 = commit
606 606 commit1 = commit.first_parent
607 607
608 608 range_diff_cache_file_path = diff_cache_exist(
609 609 cache_path, 'diff', commit.raw_id,
610 610 ign_whitespace_lcl, context_lcl, c.fulldiff)
611 611
612 612 cached_diff = None
613 613 if caching_enabled:
614 614 cached_diff = load_cached_diff(range_diff_cache_file_path)
615 615
616 616 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
617 617 if not force_recache and has_proper_diff_cache:
618 618 diffset = cached_diff['diff']
619 619 else:
620 620 diffset = self._get_range_diffset(
621 621 source_scm, source_repo,
622 622 commit1, commit2, diff_limit, file_limit,
623 623 c.fulldiff, ign_whitespace_lcl, context_lcl
624 624 )
625 625
626 626 # save cached diff
627 627 if caching_enabled:
628 628 cache_diff(range_diff_cache_file_path, diffset, None)
629 629
630 630 c.changes[commit.raw_id] = diffset
631 631
632 632 # this is a hack to properly display links, when creating PR, the
633 633 # compare view and others uses different notation, and
634 634 # compare_commits.mako renders links based on the target_repo.
635 635 # We need to swap that here to generate it properly on the html side
636 636 c.target_repo = c.source_repo
637 637
638 638 c.commit_statuses = ChangesetStatus.STATUSES
639 639
640 640 c.show_version_changes = not pr_closed
641 641 if c.show_version_changes:
642 642 cur_obj = pull_request_at_ver
643 643 prev_obj = prev_pull_request_at_ver
644 644
645 645 old_commit_ids = prev_obj.revisions
646 646 new_commit_ids = cur_obj.revisions
647 647 commit_changes = PullRequestModel()._calculate_commit_id_changes(
648 648 old_commit_ids, new_commit_ids)
649 649 c.commit_changes_summary = commit_changes
650 650
651 651 # calculate the diff for commits between versions
652 652 c.commit_changes = []
653 653 mark = lambda cs, fw: list(
654 654 h.itertools.izip_longest([], cs, fillvalue=fw))
655 655 for c_type, raw_id in mark(commit_changes.added, 'a') \
656 656 + mark(commit_changes.removed, 'r') \
657 657 + mark(commit_changes.common, 'c'):
658 658
659 659 if raw_id in commit_cache:
660 660 commit = commit_cache[raw_id]
661 661 else:
662 662 try:
663 663 commit = commits_source_repo.get_commit(raw_id)
664 664 except CommitDoesNotExistError:
665 665 # in case we fail extracting still use "dummy" commit
666 666 # for display in commit diff
667 667 commit = h.AttributeDict(
668 668 {'raw_id': raw_id,
669 669 'message': 'EMPTY or MISSING COMMIT'})
670 670 c.commit_changes.append([c_type, commit])
671 671
672 672 # current user review statuses for each version
673 673 c.review_versions = {}
674 674 if self._rhodecode_user.user_id in allowed_reviewers:
675 675 for co in general_comments:
676 676 if co.author.user_id == self._rhodecode_user.user_id:
677 677 status = co.status_change
678 678 if status:
679 679 _ver_pr = status[0].comment.pull_request_version_id
680 680 c.review_versions[_ver_pr] = status[0]
681 681
682 682 return self._get_template_context(c)
683 683
684 684 def get_commits(
685 685 self, commits_source_repo, pull_request_at_ver, source_commit,
686 686 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
687 687 commit_cache = collections.OrderedDict()
688 688 missing_requirements = False
689 689 try:
690 690 pre_load = ["author", "branch", "date", "message", "parents"]
691 691 show_revs = pull_request_at_ver.revisions
692 692 for rev in show_revs:
693 693 comm = commits_source_repo.get_commit(
694 694 commit_id=rev, pre_load=pre_load)
695 695 commit_cache[comm.raw_id] = comm
696 696
697 697 # Order here matters, we first need to get target, and then
698 698 # the source
699 699 target_commit = commits_source_repo.get_commit(
700 700 commit_id=safe_str(target_ref_id))
701 701
702 702 source_commit = commits_source_repo.get_commit(
703 703 commit_id=safe_str(source_ref_id))
704 704 except CommitDoesNotExistError:
705 705 log.warning(
706 706 'Failed to get commit from `{}` repo'.format(
707 707 commits_source_repo), exc_info=True)
708 708 except RepositoryRequirementError:
709 709 log.warning(
710 710 'Failed to get all required data from repo', exc_info=True)
711 711 missing_requirements = True
712 712 ancestor_commit = None
713 713 try:
714 714 ancestor_id = source_scm.get_common_ancestor(
715 715 source_commit.raw_id, target_commit.raw_id, target_scm)
716 716 ancestor_commit = source_scm.get_commit(ancestor_id)
717 717 except Exception:
718 718 ancestor_commit = None
719 719 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
720 720
721 721 def assure_not_empty_repo(self):
722 722 _ = self.request.translate
723 723
724 724 try:
725 725 self.db_repo.scm_instance().get_commit()
726 726 except EmptyRepositoryError:
727 727 h.flash(h.literal(_('There are no commits yet')),
728 728 category='warning')
729 729 raise HTTPFound(
730 730 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
731 731
732 732 @LoginRequired()
733 733 @NotAnonymous()
734 734 @HasRepoPermissionAnyDecorator(
735 735 'repository.read', 'repository.write', 'repository.admin')
736 736 @view_config(
737 737 route_name='pullrequest_new', request_method='GET',
738 738 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
739 739 def pull_request_new(self):
740 740 _ = self.request.translate
741 741 c = self.load_default_context()
742 742
743 743 self.assure_not_empty_repo()
744 744 source_repo = self.db_repo
745 745
746 746 commit_id = self.request.GET.get('commit')
747 747 branch_ref = self.request.GET.get('branch')
748 748 bookmark_ref = self.request.GET.get('bookmark')
749 749
750 750 try:
751 751 source_repo_data = PullRequestModel().generate_repo_data(
752 752 source_repo, commit_id=commit_id,
753 753 branch=branch_ref, bookmark=bookmark_ref,
754 754 translator=self.request.translate)
755 755 except CommitDoesNotExistError as e:
756 756 log.exception(e)
757 757 h.flash(_('Commit does not exist'), 'error')
758 758 raise HTTPFound(
759 759 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
760 760
761 761 default_target_repo = source_repo
762 762
763 763 if source_repo.parent and c.has_origin_repo_read_perm:
764 764 parent_vcs_obj = source_repo.parent.scm_instance()
765 765 if parent_vcs_obj and not parent_vcs_obj.is_empty():
766 766 # change default if we have a parent repo
767 767 default_target_repo = source_repo.parent
768 768
769 769 target_repo_data = PullRequestModel().generate_repo_data(
770 770 default_target_repo, translator=self.request.translate)
771 771
772 772 selected_source_ref = source_repo_data['refs']['selected_ref']
773 773 title_source_ref = ''
774 774 if selected_source_ref:
775 775 title_source_ref = selected_source_ref.split(':', 2)[1]
776 776 c.default_title = PullRequestModel().generate_pullrequest_title(
777 777 source=source_repo.repo_name,
778 778 source_ref=title_source_ref,
779 779 target=default_target_repo.repo_name
780 780 )
781 781
782 782 c.default_repo_data = {
783 783 'source_repo_name': source_repo.repo_name,
784 784 'source_refs_json': json.dumps(source_repo_data),
785 785 'target_repo_name': default_target_repo.repo_name,
786 786 'target_refs_json': json.dumps(target_repo_data),
787 787 }
788 788 c.default_source_ref = selected_source_ref
789 789
790 790 return self._get_template_context(c)
791 791
792 792 @LoginRequired()
793 793 @NotAnonymous()
794 794 @HasRepoPermissionAnyDecorator(
795 795 'repository.read', 'repository.write', 'repository.admin')
796 796 @view_config(
797 797 route_name='pullrequest_repo_refs', request_method='GET',
798 798 renderer='json_ext', xhr=True)
799 799 def pull_request_repo_refs(self):
800 800 self.load_default_context()
801 801 target_repo_name = self.request.matchdict['target_repo_name']
802 802 repo = Repository.get_by_repo_name(target_repo_name)
803 803 if not repo:
804 804 raise HTTPNotFound()
805 805
806 806 target_perm = HasRepoPermissionAny(
807 807 'repository.read', 'repository.write', 'repository.admin')(
808 808 target_repo_name)
809 809 if not target_perm:
810 810 raise HTTPNotFound()
811 811
812 812 return PullRequestModel().generate_repo_data(
813 813 repo, translator=self.request.translate)
814 814
815 815 @LoginRequired()
816 816 @NotAnonymous()
817 817 @HasRepoPermissionAnyDecorator(
818 818 'repository.read', 'repository.write', 'repository.admin')
819 819 @view_config(
820 820 route_name='pullrequest_repo_targets', request_method='GET',
821 821 renderer='json_ext', xhr=True)
822 822 def pullrequest_repo_targets(self):
823 823 _ = self.request.translate
824 824 filter_query = self.request.GET.get('query')
825 825
826 826 # get the parents
827 827 parent_target_repos = []
828 828 if self.db_repo.parent:
829 829 parents_query = Repository.query() \
830 830 .order_by(func.length(Repository.repo_name)) \
831 831 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
832 832
833 833 if filter_query:
834 834 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
835 835 parents_query = parents_query.filter(
836 836 Repository.repo_name.ilike(ilike_expression))
837 837 parents = parents_query.limit(20).all()
838 838
839 839 for parent in parents:
840 840 parent_vcs_obj = parent.scm_instance()
841 841 if parent_vcs_obj and not parent_vcs_obj.is_empty():
842 842 parent_target_repos.append(parent)
843 843
844 844 # get other forks, and repo itself
845 845 query = Repository.query() \
846 846 .order_by(func.length(Repository.repo_name)) \
847 847 .filter(
848 848 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
849 849 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
850 850 ) \
851 851 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
852 852
853 853 if filter_query:
854 854 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
855 855 query = query.filter(Repository.repo_name.ilike(ilike_expression))
856 856
857 857 limit = max(20 - len(parent_target_repos), 5) # not less then 5
858 858 target_repos = query.limit(limit).all()
859 859
860 860 all_target_repos = target_repos + parent_target_repos
861 861
862 862 repos = []
863 863 # This checks permissions to the repositories
864 864 for obj in ScmModel().get_repos(all_target_repos):
865 865 repos.append({
866 866 'id': obj['name'],
867 867 'text': obj['name'],
868 868 'type': 'repo',
869 869 'repo_id': obj['dbrepo']['repo_id'],
870 870 'repo_type': obj['dbrepo']['repo_type'],
871 871 'private': obj['dbrepo']['private'],
872 872
873 873 })
874 874
875 875 data = {
876 876 'more': False,
877 877 'results': [{
878 878 'text': _('Repositories'),
879 879 'children': repos
880 880 }] if repos else []
881 881 }
882 882 return data
883 883
884 884 @LoginRequired()
885 885 @NotAnonymous()
886 886 @HasRepoPermissionAnyDecorator(
887 887 'repository.read', 'repository.write', 'repository.admin')
888 888 @CSRFRequired()
889 889 @view_config(
890 890 route_name='pullrequest_create', request_method='POST',
891 891 renderer=None)
892 892 def pull_request_create(self):
893 893 _ = self.request.translate
894 894 self.assure_not_empty_repo()
895 895 self.load_default_context()
896 896
897 897 controls = peppercorn.parse(self.request.POST.items())
898 898
899 899 try:
900 900 form = PullRequestForm(
901 901 self.request.translate, self.db_repo.repo_id)()
902 902 _form = form.to_python(controls)
903 903 except formencode.Invalid as errors:
904 904 if errors.error_dict.get('revisions'):
905 905 msg = 'Revisions: %s' % errors.error_dict['revisions']
906 906 elif errors.error_dict.get('pullrequest_title'):
907 907 msg = errors.error_dict.get('pullrequest_title')
908 908 else:
909 909 msg = _('Error creating pull request: {}').format(errors)
910 910 log.exception(msg)
911 911 h.flash(msg, 'error')
912 912
913 913 # would rather just go back to form ...
914 914 raise HTTPFound(
915 915 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
916 916
917 917 source_repo = _form['source_repo']
918 918 source_ref = _form['source_ref']
919 919 target_repo = _form['target_repo']
920 920 target_ref = _form['target_ref']
921 921 commit_ids = _form['revisions'][::-1]
922 922
923 923 # find the ancestor for this pr
924 924 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
925 925 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
926 926
927 927 if not (source_db_repo or target_db_repo):
928 928 h.flash(_('source_repo or target repo not found'), category='error')
929 929 raise HTTPFound(
930 930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
931 931
932 932 # re-check permissions again here
933 933 # source_repo we must have read permissions
934 934
935 935 source_perm = HasRepoPermissionAny(
936 936 'repository.read', 'repository.write', 'repository.admin')(
937 937 source_db_repo.repo_name)
938 938 if not source_perm:
939 939 msg = _('Not Enough permissions to source repo `{}`.'.format(
940 940 source_db_repo.repo_name))
941 941 h.flash(msg, category='error')
942 942 # copy the args back to redirect
943 943 org_query = self.request.GET.mixed()
944 944 raise HTTPFound(
945 945 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
946 946 _query=org_query))
947 947
948 948 # target repo we must have read permissions, and also later on
949 949 # we want to check branch permissions here
950 950 target_perm = HasRepoPermissionAny(
951 951 'repository.read', 'repository.write', 'repository.admin')(
952 952 target_db_repo.repo_name)
953 953 if not target_perm:
954 954 msg = _('Not Enough permissions to target repo `{}`.'.format(
955 955 target_db_repo.repo_name))
956 956 h.flash(msg, category='error')
957 957 # copy the args back to redirect
958 958 org_query = self.request.GET.mixed()
959 959 raise HTTPFound(
960 960 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
961 961 _query=org_query))
962 962
963 963 source_scm = source_db_repo.scm_instance()
964 964 target_scm = target_db_repo.scm_instance()
965 965
966 966 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
967 967 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
968 968
969 969 ancestor = source_scm.get_common_ancestor(
970 970 source_commit.raw_id, target_commit.raw_id, target_scm)
971 971
972 972 # recalculate target ref based on ancestor
973 973 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
974 974 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
975 975
976 976 get_default_reviewers_data, validate_default_reviewers = \
977 977 PullRequestModel().get_reviewer_functions()
978 978
979 979 # recalculate reviewers logic, to make sure we can validate this
980 980 reviewer_rules = get_default_reviewers_data(
981 981 self._rhodecode_db_user, source_db_repo,
982 982 source_commit, target_db_repo, target_commit)
983 983
984 984 given_reviewers = _form['review_members']
985 985 reviewers = validate_default_reviewers(
986 986 given_reviewers, reviewer_rules)
987 987
988 988 pullrequest_title = _form['pullrequest_title']
989 989 title_source_ref = source_ref.split(':', 2)[1]
990 990 if not pullrequest_title:
991 991 pullrequest_title = PullRequestModel().generate_pullrequest_title(
992 992 source=source_repo,
993 993 source_ref=title_source_ref,
994 994 target=target_repo
995 995 )
996 996
997 997 description = _form['pullrequest_desc']
998 998 description_renderer = _form['description_renderer']
999 999
1000 1000 try:
1001 1001 pull_request = PullRequestModel().create(
1002 1002 created_by=self._rhodecode_user.user_id,
1003 1003 source_repo=source_repo,
1004 1004 source_ref=source_ref,
1005 1005 target_repo=target_repo,
1006 1006 target_ref=target_ref,
1007 1007 revisions=commit_ids,
1008 1008 reviewers=reviewers,
1009 1009 title=pullrequest_title,
1010 1010 description=description,
1011 1011 description_renderer=description_renderer,
1012 1012 reviewer_data=reviewer_rules,
1013 1013 auth_user=self._rhodecode_user
1014 1014 )
1015 1015 Session().commit()
1016 1016
1017 1017 h.flash(_('Successfully opened new pull request'),
1018 1018 category='success')
1019 1019 except Exception:
1020 1020 msg = _('Error occurred during creation of this pull request.')
1021 1021 log.exception(msg)
1022 1022 h.flash(msg, category='error')
1023 1023
1024 1024 # copy the args back to redirect
1025 1025 org_query = self.request.GET.mixed()
1026 1026 raise HTTPFound(
1027 1027 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1028 1028 _query=org_query))
1029 1029
1030 1030 raise HTTPFound(
1031 1031 h.route_path('pullrequest_show', repo_name=target_repo,
1032 1032 pull_request_id=pull_request.pull_request_id))
1033 1033
1034 1034 @LoginRequired()
1035 1035 @NotAnonymous()
1036 1036 @HasRepoPermissionAnyDecorator(
1037 1037 'repository.read', 'repository.write', 'repository.admin')
1038 1038 @CSRFRequired()
1039 1039 @view_config(
1040 1040 route_name='pullrequest_update', request_method='POST',
1041 1041 renderer='json_ext')
1042 1042 def pull_request_update(self):
1043 1043 pull_request = PullRequest.get_or_404(
1044 1044 self.request.matchdict['pull_request_id'])
1045 1045 _ = self.request.translate
1046 1046
1047 1047 self.load_default_context()
1048 1048
1049 1049 if pull_request.is_closed():
1050 1050 log.debug('update: forbidden because pull request is closed')
1051 1051 msg = _(u'Cannot update closed pull requests.')
1052 1052 h.flash(msg, category='error')
1053 1053 return True
1054 1054
1055 1055 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1056 1056 log.debug('update: forbidden because pull request is in state %s',
1057 1057 pull_request.pull_request_state)
1058 1058 msg = _(u'Cannot update pull requests in state other than `{}`. '
1059 1059 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1060 1060 pull_request.pull_request_state)
1061 1061 h.flash(msg, category='error')
1062 1062 return True
1063 1063
1064 1064 # only owner or admin can update it
1065 1065 allowed_to_update = PullRequestModel().check_user_update(
1066 1066 pull_request, self._rhodecode_user)
1067 1067 if allowed_to_update:
1068 1068 controls = peppercorn.parse(self.request.POST.items())
1069 1069
1070 1070 if 'review_members' in controls:
1071 1071 self._update_reviewers(
1072 1072 pull_request, controls['review_members'],
1073 1073 pull_request.reviewer_data)
1074 1074 elif str2bool(self.request.POST.get('update_commits', 'false')):
1075 1075 self._update_commits(pull_request)
1076 1076 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1077 1077 self._edit_pull_request(pull_request)
1078 1078 else:
1079 1079 raise HTTPBadRequest()
1080 1080 return True
1081 1081 raise HTTPForbidden()
1082 1082
1083 1083 def _edit_pull_request(self, pull_request):
1084 1084 _ = self.request.translate
1085 1085
1086 1086 try:
1087 1087 PullRequestModel().edit(
1088 1088 pull_request,
1089 1089 self.request.POST.get('title'),
1090 1090 self.request.POST.get('description'),
1091 1091 self.request.POST.get('description_renderer'),
1092 1092 self._rhodecode_user)
1093 1093 except ValueError:
1094 1094 msg = _(u'Cannot update closed pull requests.')
1095 1095 h.flash(msg, category='error')
1096 1096 return
1097 1097 else:
1098 1098 Session().commit()
1099 1099
1100 1100 msg = _(u'Pull request title & description updated.')
1101 1101 h.flash(msg, category='success')
1102 1102 return
1103 1103
1104 1104 def _update_commits(self, pull_request):
1105 1105 _ = self.request.translate
1106 1106
1107 1107 with pull_request.set_state(PullRequest.STATE_UPDATING):
1108 1108 resp = PullRequestModel().update_commits(pull_request)
1109 1109
1110 1110 if resp.executed:
1111 1111
1112 1112 if resp.target_changed and resp.source_changed:
1113 1113 changed = 'target and source repositories'
1114 1114 elif resp.target_changed and not resp.source_changed:
1115 1115 changed = 'target repository'
1116 1116 elif not resp.target_changed and resp.source_changed:
1117 1117 changed = 'source repository'
1118 1118 else:
1119 1119 changed = 'nothing'
1120 1120
1121 1121 msg = _(u'Pull request updated to "{source_commit_id}" with '
1122 1122 u'{count_added} added, {count_removed} removed commits. '
1123 1123 u'Source of changes: {change_source}')
1124 1124 msg = msg.format(
1125 1125 source_commit_id=pull_request.source_ref_parts.commit_id,
1126 1126 count_added=len(resp.changes.added),
1127 1127 count_removed=len(resp.changes.removed),
1128 1128 change_source=changed)
1129 1129 h.flash(msg, category='success')
1130 1130
1131 1131 channel = '/repo${}$/pr/{}'.format(
1132 1132 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1133 1133 message = msg + (
1134 1134 ' - <a onclick="window.location.reload()">'
1135 1135 '<strong>{}</strong></a>'.format(_('Reload page')))
1136 1136 channelstream.post_message(
1137 1137 channel, message, self._rhodecode_user.username,
1138 1138 registry=self.request.registry)
1139 1139 else:
1140 1140 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1141 1141 warning_reasons = [
1142 1142 UpdateFailureReason.NO_CHANGE,
1143 1143 UpdateFailureReason.WRONG_REF_TYPE,
1144 1144 ]
1145 1145 category = 'warning' if resp.reason in warning_reasons else 'error'
1146 1146 h.flash(msg, category=category)
1147 1147
1148 1148 @LoginRequired()
1149 1149 @NotAnonymous()
1150 1150 @HasRepoPermissionAnyDecorator(
1151 1151 'repository.read', 'repository.write', 'repository.admin')
1152 1152 @CSRFRequired()
1153 1153 @view_config(
1154 1154 route_name='pullrequest_merge', request_method='POST',
1155 1155 renderer='json_ext')
1156 1156 def pull_request_merge(self):
1157 1157 """
1158 1158 Merge will perform a server-side merge of the specified
1159 1159 pull request, if the pull request is approved and mergeable.
1160 1160 After successful merging, the pull request is automatically
1161 1161 closed, with a relevant comment.
1162 1162 """
1163 1163 pull_request = PullRequest.get_or_404(
1164 1164 self.request.matchdict['pull_request_id'])
1165 1165 _ = self.request.translate
1166 1166
1167 1167 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1168 1168 log.debug('show: forbidden because pull request is in state %s',
1169 1169 pull_request.pull_request_state)
1170 1170 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1171 1171 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1172 1172 pull_request.pull_request_state)
1173 1173 h.flash(msg, category='error')
1174 1174 raise HTTPFound(
1175 1175 h.route_path('pullrequest_show',
1176 1176 repo_name=pull_request.target_repo.repo_name,
1177 1177 pull_request_id=pull_request.pull_request_id))
1178 1178
1179 1179 self.load_default_context()
1180 1180
1181 1181 with pull_request.set_state(PullRequest.STATE_UPDATING):
1182 1182 check = MergeCheck.validate(
1183 1183 pull_request, auth_user=self._rhodecode_user,
1184 1184 translator=self.request.translate)
1185 1185 merge_possible = not check.failed
1186 1186
1187 1187 for err_type, error_msg in check.errors:
1188 1188 h.flash(error_msg, category=err_type)
1189 1189
1190 1190 if merge_possible:
1191 1191 log.debug("Pre-conditions checked, trying to merge.")
1192 1192 extras = vcs_operation_context(
1193 1193 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1194 1194 username=self._rhodecode_db_user.username, action='push',
1195 1195 scm=pull_request.target_repo.repo_type)
1196 1196 with pull_request.set_state(PullRequest.STATE_UPDATING):
1197 1197 self._merge_pull_request(
1198 1198 pull_request, self._rhodecode_db_user, extras)
1199 1199 else:
1200 1200 log.debug("Pre-conditions failed, NOT merging.")
1201 1201
1202 1202 raise HTTPFound(
1203 1203 h.route_path('pullrequest_show',
1204 1204 repo_name=pull_request.target_repo.repo_name,
1205 1205 pull_request_id=pull_request.pull_request_id))
1206 1206
1207 1207 def _merge_pull_request(self, pull_request, user, extras):
1208 1208 _ = self.request.translate
1209 1209 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1210 1210
1211 1211 if merge_resp.executed:
1212 1212 log.debug("The merge was successful, closing the pull request.")
1213 1213 PullRequestModel().close_pull_request(
1214 1214 pull_request.pull_request_id, user)
1215 1215 Session().commit()
1216 1216 msg = _('Pull request was successfully merged and closed.')
1217 1217 h.flash(msg, category='success')
1218 1218 else:
1219 1219 log.debug(
1220 1220 "The merge was not successful. Merge response: %s", merge_resp)
1221 1221 msg = merge_resp.merge_status_message
1222 1222 h.flash(msg, category='error')
1223 1223
1224 1224 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1225 1225 _ = self.request.translate
1226 1226
1227 1227 get_default_reviewers_data, validate_default_reviewers = \
1228 1228 PullRequestModel().get_reviewer_functions()
1229 1229
1230 1230 try:
1231 1231 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1232 1232 except ValueError as e:
1233 1233 log.error('Reviewers Validation: {}'.format(e))
1234 1234 h.flash(e, category='error')
1235 1235 return
1236 1236
1237 1237 old_calculated_status = pull_request.calculated_review_status()
1238 1238 PullRequestModel().update_reviewers(
1239 1239 pull_request, reviewers, self._rhodecode_user)
1240 1240 h.flash(_('Pull request reviewers updated.'), category='success')
1241 1241 Session().commit()
1242 1242
1243 1243 # trigger status changed if change in reviewers changes the status
1244 1244 calculated_status = pull_request.calculated_review_status()
1245 1245 if old_calculated_status != calculated_status:
1246 1246 PullRequestModel().trigger_pull_request_hook(
1247 1247 pull_request, self._rhodecode_user, 'review_status_change',
1248 1248 data={'status': calculated_status})
1249 1249
1250 1250 @LoginRequired()
1251 1251 @NotAnonymous()
1252 1252 @HasRepoPermissionAnyDecorator(
1253 1253 'repository.read', 'repository.write', 'repository.admin')
1254 1254 @CSRFRequired()
1255 1255 @view_config(
1256 1256 route_name='pullrequest_delete', request_method='POST',
1257 1257 renderer='json_ext')
1258 1258 def pull_request_delete(self):
1259 1259 _ = self.request.translate
1260 1260
1261 1261 pull_request = PullRequest.get_or_404(
1262 1262 self.request.matchdict['pull_request_id'])
1263 1263 self.load_default_context()
1264 1264
1265 1265 pr_closed = pull_request.is_closed()
1266 1266 allowed_to_delete = PullRequestModel().check_user_delete(
1267 1267 pull_request, self._rhodecode_user) and not pr_closed
1268 1268
1269 1269 # only owner can delete it !
1270 1270 if allowed_to_delete:
1271 1271 PullRequestModel().delete(pull_request, self._rhodecode_user)
1272 1272 Session().commit()
1273 1273 h.flash(_('Successfully deleted pull request'),
1274 1274 category='success')
1275 1275 raise HTTPFound(h.route_path('pullrequest_show_all',
1276 1276 repo_name=self.db_repo_name))
1277 1277
1278 1278 log.warning('user %s tried to delete pull request without access',
1279 1279 self._rhodecode_user)
1280 1280 raise HTTPNotFound()
1281 1281
1282 1282 @LoginRequired()
1283 1283 @NotAnonymous()
1284 1284 @HasRepoPermissionAnyDecorator(
1285 1285 'repository.read', 'repository.write', 'repository.admin')
1286 1286 @CSRFRequired()
1287 1287 @view_config(
1288 1288 route_name='pullrequest_comment_create', request_method='POST',
1289 1289 renderer='json_ext')
1290 1290 def pull_request_comment_create(self):
1291 1291 _ = self.request.translate
1292 1292
1293 1293 pull_request = PullRequest.get_or_404(
1294 1294 self.request.matchdict['pull_request_id'])
1295 1295 pull_request_id = pull_request.pull_request_id
1296 1296
1297 1297 if pull_request.is_closed():
1298 1298 log.debug('comment: forbidden because pull request is closed')
1299 1299 raise HTTPForbidden()
1300 1300
1301 1301 allowed_to_comment = PullRequestModel().check_user_comment(
1302 1302 pull_request, self._rhodecode_user)
1303 1303 if not allowed_to_comment:
1304 1304 log.debug(
1305 1305 'comment: forbidden because pull request is from forbidden repo')
1306 1306 raise HTTPForbidden()
1307 1307
1308 1308 c = self.load_default_context()
1309 1309
1310 1310 status = self.request.POST.get('changeset_status', None)
1311 1311 text = self.request.POST.get('text')
1312 1312 comment_type = self.request.POST.get('comment_type')
1313 1313 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1314 1314 close_pull_request = self.request.POST.get('close_pull_request')
1315 1315
1316 1316 # the logic here should work like following, if we submit close
1317 1317 # pr comment, use `close_pull_request_with_comment` function
1318 1318 # else handle regular comment logic
1319 1319
1320 1320 if close_pull_request:
1321 1321 # only owner or admin or person with write permissions
1322 1322 allowed_to_close = PullRequestModel().check_user_update(
1323 1323 pull_request, self._rhodecode_user)
1324 1324 if not allowed_to_close:
1325 1325 log.debug('comment: forbidden because not allowed to close '
1326 1326 'pull request %s', pull_request_id)
1327 1327 raise HTTPForbidden()
1328 1328
1329 1329 # This also triggers `review_status_change`
1330 1330 comment, status = PullRequestModel().close_pull_request_with_comment(
1331 1331 pull_request, self._rhodecode_user, self.db_repo, message=text,
1332 1332 auth_user=self._rhodecode_user)
1333 1333 Session().flush()
1334 1334
1335 1335 PullRequestModel().trigger_pull_request_hook(
1336 1336 pull_request, self._rhodecode_user, 'comment',
1337 1337 data={'comment': comment})
1338 1338
1339 1339 else:
1340 1340 # regular comment case, could be inline, or one with status.
1341 1341 # for that one we check also permissions
1342 1342
1343 1343 allowed_to_change_status = PullRequestModel().check_user_change_status(
1344 1344 pull_request, self._rhodecode_user)
1345 1345
1346 1346 if status and allowed_to_change_status:
1347 1347 message = (_('Status change %(transition_icon)s %(status)s')
1348 1348 % {'transition_icon': '>',
1349 1349 'status': ChangesetStatus.get_status_lbl(status)})
1350 1350 text = text or message
1351 1351
1352 1352 comment = CommentsModel().create(
1353 1353 text=text,
1354 1354 repo=self.db_repo.repo_id,
1355 1355 user=self._rhodecode_user.user_id,
1356 1356 pull_request=pull_request,
1357 1357 f_path=self.request.POST.get('f_path'),
1358 1358 line_no=self.request.POST.get('line'),
1359 1359 status_change=(ChangesetStatus.get_status_lbl(status)
1360 1360 if status and allowed_to_change_status else None),
1361 1361 status_change_type=(status
1362 1362 if status and allowed_to_change_status else None),
1363 1363 comment_type=comment_type,
1364 1364 resolves_comment_id=resolves_comment_id,
1365 1365 auth_user=self._rhodecode_user
1366 1366 )
1367 1367
1368 1368 if allowed_to_change_status:
1369 1369 # calculate old status before we change it
1370 1370 old_calculated_status = pull_request.calculated_review_status()
1371 1371
1372 1372 # get status if set !
1373 1373 if status:
1374 1374 ChangesetStatusModel().set_status(
1375 1375 self.db_repo.repo_id,
1376 1376 status,
1377 1377 self._rhodecode_user.user_id,
1378 1378 comment,
1379 1379 pull_request=pull_request
1380 1380 )
1381 1381
1382 1382 Session().flush()
1383 1383 # this is somehow required to get access to some relationship
1384 1384 # loaded on comment
1385 1385 Session().refresh(comment)
1386 1386
1387 1387 PullRequestModel().trigger_pull_request_hook(
1388 1388 pull_request, self._rhodecode_user, 'comment',
1389 1389 data={'comment': comment})
1390 1390
1391 1391 # we now calculate the status of pull request, and based on that
1392 1392 # calculation we set the commits status
1393 1393 calculated_status = pull_request.calculated_review_status()
1394 1394 if old_calculated_status != calculated_status:
1395 1395 PullRequestModel().trigger_pull_request_hook(
1396 1396 pull_request, self._rhodecode_user, 'review_status_change',
1397 1397 data={'status': calculated_status})
1398 1398
1399 1399 Session().commit()
1400 1400
1401 1401 data = {
1402 1402 'target_id': h.safeid(h.safe_unicode(
1403 1403 self.request.POST.get('f_path'))),
1404 1404 }
1405 1405 if comment:
1406 1406 c.co = comment
1407 1407 rendered_comment = render(
1408 1408 'rhodecode:templates/changeset/changeset_comment_block.mako',
1409 1409 self._get_template_context(c), self.request)
1410 1410
1411 1411 data.update(comment.get_dict())
1412 1412 data.update({'rendered_text': rendered_comment})
1413 1413
1414 1414 return data
1415 1415
1416 1416 @LoginRequired()
1417 1417 @NotAnonymous()
1418 1418 @HasRepoPermissionAnyDecorator(
1419 1419 'repository.read', 'repository.write', 'repository.admin')
1420 1420 @CSRFRequired()
1421 1421 @view_config(
1422 1422 route_name='pullrequest_comment_delete', request_method='POST',
1423 1423 renderer='json_ext')
1424 1424 def pull_request_comment_delete(self):
1425 1425 pull_request = PullRequest.get_or_404(
1426 1426 self.request.matchdict['pull_request_id'])
1427 1427
1428 1428 comment = ChangesetComment.get_or_404(
1429 1429 self.request.matchdict['comment_id'])
1430 1430 comment_id = comment.comment_id
1431 1431
1432 1432 if pull_request.is_closed():
1433 1433 log.debug('comment: forbidden because pull request is closed')
1434 1434 raise HTTPForbidden()
1435 1435
1436 1436 if not comment:
1437 1437 log.debug('Comment with id:%s not found, skipping', comment_id)
1438 1438 # comment already deleted in another call probably
1439 1439 return True
1440 1440
1441 1441 if comment.pull_request.is_closed():
1442 1442 # don't allow deleting comments on closed pull request
1443 1443 raise HTTPForbidden()
1444 1444
1445 1445 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1446 1446 super_admin = h.HasPermissionAny('hg.admin')()
1447 1447 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1448 1448 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1449 1449 comment_repo_admin = is_repo_admin and is_repo_comment
1450 1450
1451 1451 if super_admin or comment_owner or comment_repo_admin:
1452 1452 old_calculated_status = comment.pull_request.calculated_review_status()
1453 1453 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1454 1454 Session().commit()
1455 1455 calculated_status = comment.pull_request.calculated_review_status()
1456 1456 if old_calculated_status != calculated_status:
1457 1457 PullRequestModel().trigger_pull_request_hook(
1458 1458 comment.pull_request, self._rhodecode_user, 'review_status_change',
1459 1459 data={'status': calculated_status})
1460 1460 return True
1461 1461 else:
1462 1462 log.warning('No permissions for user %s to delete comment_id: %s',
1463 1463 self._rhodecode_db_user, comment_id)
1464 1464 raise HTTPNotFound()
@@ -1,1742 +1,1742 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31 import collections
32 32
33 33 from pyramid import compat
34 34 from pyramid.threadlocal import get_current_request
35 35
36 36 from rhodecode import events
37 37 from rhodecode.translation import lazy_ugettext
38 38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 39 from rhodecode.lib import audit_logger
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 42 from rhodecode.lib.markup_renderer import (
43 43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 45 from rhodecode.lib.vcs.backends.base import (
46 46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 48 from rhodecode.lib.vcs.exceptions import (
49 49 CommitDoesNotExistError, EmptyRepositoryError)
50 50 from rhodecode.model import BaseModel
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.comment import CommentsModel
53 53 from rhodecode.model.db import (
54 54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 56 from rhodecode.model.meta import Session
57 57 from rhodecode.model.notification import NotificationModel, \
58 58 EmailNotificationModel
59 59 from rhodecode.model.scm import ScmModel
60 60 from rhodecode.model.settings import VcsSettingsModel
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # Data structure to hold the response data when updating commits during a pull
67 67 # request update.
68 68 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 69 'executed', 'reason', 'new', 'old', 'changes',
70 70 'source_changed', 'target_changed'])
71 71
72 72
73 73 class PullRequestModel(BaseModel):
74 74
75 75 cls = PullRequest
76 76
77 77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78 78
79 79 UPDATE_STATUS_MESSAGES = {
80 80 UpdateFailureReason.NONE: lazy_ugettext(
81 81 'Pull request update successful.'),
82 82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 83 'Pull request update failed because of an unknown error.'),
84 84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 85 'No update needed because the source and target have not changed.'),
86 86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 87 'Pull request cannot be updated because the reference type is '
88 88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 90 'This pull request cannot be updated because the target '
91 91 'reference is missing.'),
92 92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 93 'This pull request cannot be updated because the source '
94 94 'reference is missing.'),
95 95 }
96 96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98 98
99 99 def __get_pull_request(self, pull_request):
100 100 return self._get_instance((
101 101 PullRequest, PullRequestVersion), pull_request)
102 102
103 103 def _check_perms(self, perms, pull_request, user, api=False):
104 104 if not api:
105 105 return h.HasRepoPermissionAny(*perms)(
106 106 user=user, repo_name=pull_request.target_repo.repo_name)
107 107 else:
108 108 return h.HasRepoPermissionAnyApi(*perms)(
109 109 user=user, repo_name=pull_request.target_repo.repo_name)
110 110
111 111 def check_user_read(self, pull_request, user, api=False):
112 112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 113 return self._check_perms(_perms, pull_request, user, api)
114 114
115 115 def check_user_merge(self, pull_request, user, api=False):
116 116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 117 return self._check_perms(_perms, pull_request, user, api)
118 118
119 119 def check_user_update(self, pull_request, user, api=False):
120 120 owner = user.user_id == pull_request.user_id
121 121 return self.check_user_merge(pull_request, user, api) or owner
122 122
123 123 def check_user_delete(self, pull_request, user):
124 124 owner = user.user_id == pull_request.user_id
125 125 _perms = ('repository.admin',)
126 126 return self._check_perms(_perms, pull_request, user) or owner
127 127
128 128 def check_user_change_status(self, pull_request, user, api=False):
129 129 reviewer = user.user_id in [x.user_id for x in
130 130 pull_request.reviewers]
131 131 return self.check_user_update(pull_request, user, api) or reviewer
132 132
133 133 def check_user_comment(self, pull_request, user):
134 134 owner = user.user_id == pull_request.user_id
135 135 return self.check_user_read(pull_request, user) or owner
136 136
137 137 def get(self, pull_request):
138 138 return self.__get_pull_request(pull_request)
139 139
140 140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=True):
142 order_dir='desc', only_created=False):
143 143 repo = None
144 144 if repo_name:
145 145 repo = self._get_repo(repo_name)
146 146
147 147 q = PullRequest.query()
148 148
149 149 # source or target
150 150 if repo and source:
151 151 q = q.filter(PullRequest.source_repo == repo)
152 152 elif repo:
153 153 q = q.filter(PullRequest.target_repo == repo)
154 154
155 155 # closed,opened
156 156 if statuses:
157 157 q = q.filter(PullRequest.status.in_(statuses))
158 158
159 159 # opened by filter
160 160 if opened_by:
161 161 q = q.filter(PullRequest.user_id.in_(opened_by))
162 162
163 163 # only get those that are in "created" state
164 164 if only_created:
165 165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166 166
167 167 if order_by:
168 168 order_map = {
169 169 'name_raw': PullRequest.pull_request_id,
170 170 'id': PullRequest.pull_request_id,
171 171 'title': PullRequest.title,
172 172 'updated_on_raw': PullRequest.updated_on,
173 173 'target_repo': PullRequest.target_repo_id
174 174 }
175 175 if order_dir == 'asc':
176 176 q = q.order_by(order_map[order_by].asc())
177 177 else:
178 178 q = q.order_by(order_map[order_by].desc())
179 179
180 180 return q
181 181
182 182 def count_all(self, repo_name, source=False, statuses=None,
183 183 opened_by=None):
184 184 """
185 185 Count the number of pull requests for a specific repository.
186 186
187 187 :param repo_name: target or source repo
188 188 :param source: boolean flag to specify if repo_name refers to source
189 189 :param statuses: list of pull request statuses
190 190 :param opened_by: author user of the pull request
191 191 :returns: int number of pull requests
192 192 """
193 193 q = self._prepare_get_all_query(
194 194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
195 195
196 196 return q.count()
197 197
198 198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
199 199 offset=0, length=None, order_by=None, order_dir='desc'):
200 200 """
201 201 Get all pull requests for a specific repository.
202 202
203 203 :param repo_name: target or source repo
204 204 :param source: boolean flag to specify if repo_name refers to source
205 205 :param statuses: list of pull request statuses
206 206 :param opened_by: author user of the pull request
207 207 :param offset: pagination offset
208 208 :param length: length of returned list
209 209 :param order_by: order of the returned list
210 210 :param order_dir: 'asc' or 'desc' ordering direction
211 211 :returns: list of pull requests
212 212 """
213 213 q = self._prepare_get_all_query(
214 214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
215 215 order_by=order_by, order_dir=order_dir)
216 216
217 217 if length:
218 218 pull_requests = q.limit(length).offset(offset).all()
219 219 else:
220 220 pull_requests = q.all()
221 221
222 222 return pull_requests
223 223
224 224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
225 225 opened_by=None):
226 226 """
227 227 Count the number of pull requests for a specific repository that are
228 228 awaiting review.
229 229
230 230 :param repo_name: target or source repo
231 231 :param source: boolean flag to specify if repo_name refers to source
232 232 :param statuses: list of pull request statuses
233 233 :param opened_by: author user of the pull request
234 234 :returns: int number of pull requests
235 235 """
236 236 pull_requests = self.get_awaiting_review(
237 237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
238 238
239 239 return len(pull_requests)
240 240
241 241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
242 242 opened_by=None, offset=0, length=None,
243 243 order_by=None, order_dir='desc'):
244 244 """
245 245 Get all pull requests for a specific repository that are awaiting
246 246 review.
247 247
248 248 :param repo_name: target or source repo
249 249 :param source: boolean flag to specify if repo_name refers to source
250 250 :param statuses: list of pull request statuses
251 251 :param opened_by: author user of the pull request
252 252 :param offset: pagination offset
253 253 :param length: length of returned list
254 254 :param order_by: order of the returned list
255 255 :param order_dir: 'asc' or 'desc' ordering direction
256 256 :returns: list of pull requests
257 257 """
258 258 pull_requests = self.get_all(
259 259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
260 260 order_by=order_by, order_dir=order_dir)
261 261
262 262 _filtered_pull_requests = []
263 263 for pr in pull_requests:
264 264 status = pr.calculated_review_status()
265 265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
266 266 ChangesetStatus.STATUS_UNDER_REVIEW]:
267 267 _filtered_pull_requests.append(pr)
268 268 if length:
269 269 return _filtered_pull_requests[offset:offset+length]
270 270 else:
271 271 return _filtered_pull_requests
272 272
273 273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
274 274 opened_by=None, user_id=None):
275 275 """
276 276 Count the number of pull requests for a specific repository that are
277 277 awaiting review from a specific user.
278 278
279 279 :param repo_name: target or source repo
280 280 :param source: boolean flag to specify if repo_name refers to source
281 281 :param statuses: list of pull request statuses
282 282 :param opened_by: author user of the pull request
283 283 :param user_id: reviewer user of the pull request
284 284 :returns: int number of pull requests
285 285 """
286 286 pull_requests = self.get_awaiting_my_review(
287 287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 288 user_id=user_id)
289 289
290 290 return len(pull_requests)
291 291
292 292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
293 293 opened_by=None, user_id=None, offset=0,
294 294 length=None, order_by=None, order_dir='desc'):
295 295 """
296 296 Get all pull requests for a specific repository that are awaiting
297 297 review from a specific user.
298 298
299 299 :param repo_name: target or source repo
300 300 :param source: boolean flag to specify if repo_name refers to source
301 301 :param statuses: list of pull request statuses
302 302 :param opened_by: author user of the pull request
303 303 :param user_id: reviewer user of the pull request
304 304 :param offset: pagination offset
305 305 :param length: length of returned list
306 306 :param order_by: order of the returned list
307 307 :param order_dir: 'asc' or 'desc' ordering direction
308 308 :returns: list of pull requests
309 309 """
310 310 pull_requests = self.get_all(
311 311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 312 order_by=order_by, order_dir=order_dir)
313 313
314 314 _my = PullRequestModel().get_not_reviewed(user_id)
315 315 my_participation = []
316 316 for pr in pull_requests:
317 317 if pr in _my:
318 318 my_participation.append(pr)
319 319 _filtered_pull_requests = my_participation
320 320 if length:
321 321 return _filtered_pull_requests[offset:offset+length]
322 322 else:
323 323 return _filtered_pull_requests
324 324
325 325 def get_not_reviewed(self, user_id):
326 326 return [
327 327 x.pull_request for x in PullRequestReviewers.query().filter(
328 328 PullRequestReviewers.user_id == user_id).all()
329 329 ]
330 330
331 331 def _prepare_participating_query(self, user_id=None, statuses=None,
332 332 order_by=None, order_dir='desc'):
333 333 q = PullRequest.query()
334 334 if user_id:
335 335 reviewers_subquery = Session().query(
336 336 PullRequestReviewers.pull_request_id).filter(
337 337 PullRequestReviewers.user_id == user_id).subquery()
338 338 user_filter = or_(
339 339 PullRequest.user_id == user_id,
340 340 PullRequest.pull_request_id.in_(reviewers_subquery)
341 341 )
342 342 q = PullRequest.query().filter(user_filter)
343 343
344 344 # closed,opened
345 345 if statuses:
346 346 q = q.filter(PullRequest.status.in_(statuses))
347 347
348 348 if order_by:
349 349 order_map = {
350 350 'name_raw': PullRequest.pull_request_id,
351 351 'title': PullRequest.title,
352 352 'updated_on_raw': PullRequest.updated_on,
353 353 'target_repo': PullRequest.target_repo_id
354 354 }
355 355 if order_dir == 'asc':
356 356 q = q.order_by(order_map[order_by].asc())
357 357 else:
358 358 q = q.order_by(order_map[order_by].desc())
359 359
360 360 return q
361 361
362 362 def count_im_participating_in(self, user_id=None, statuses=None):
363 363 q = self._prepare_participating_query(user_id, statuses=statuses)
364 364 return q.count()
365 365
366 366 def get_im_participating_in(
367 367 self, user_id=None, statuses=None, offset=0,
368 368 length=None, order_by=None, order_dir='desc'):
369 369 """
370 370 Get all Pull requests that i'm participating in, or i have opened
371 371 """
372 372
373 373 q = self._prepare_participating_query(
374 374 user_id, statuses=statuses, order_by=order_by,
375 375 order_dir=order_dir)
376 376
377 377 if length:
378 378 pull_requests = q.limit(length).offset(offset).all()
379 379 else:
380 380 pull_requests = q.all()
381 381
382 382 return pull_requests
383 383
384 384 def get_versions(self, pull_request):
385 385 """
386 386 returns version of pull request sorted by ID descending
387 387 """
388 388 return PullRequestVersion.query()\
389 389 .filter(PullRequestVersion.pull_request == pull_request)\
390 390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
391 391 .all()
392 392
393 393 def get_pr_version(self, pull_request_id, version=None):
394 394 at_version = None
395 395
396 396 if version and version == 'latest':
397 397 pull_request_ver = PullRequest.get(pull_request_id)
398 398 pull_request_obj = pull_request_ver
399 399 _org_pull_request_obj = pull_request_obj
400 400 at_version = 'latest'
401 401 elif version:
402 402 pull_request_ver = PullRequestVersion.get_or_404(version)
403 403 pull_request_obj = pull_request_ver
404 404 _org_pull_request_obj = pull_request_ver.pull_request
405 405 at_version = pull_request_ver.pull_request_version_id
406 406 else:
407 407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
408 408 pull_request_id)
409 409
410 410 pull_request_display_obj = PullRequest.get_pr_display_object(
411 411 pull_request_obj, _org_pull_request_obj)
412 412
413 413 return _org_pull_request_obj, pull_request_obj, \
414 414 pull_request_display_obj, at_version
415 415
416 416 def create(self, created_by, source_repo, source_ref, target_repo,
417 417 target_ref, revisions, reviewers, title, description=None,
418 418 description_renderer=None,
419 419 reviewer_data=None, translator=None, auth_user=None):
420 420 translator = translator or get_current_request().translate
421 421
422 422 created_by_user = self._get_user(created_by)
423 423 auth_user = auth_user or created_by_user.AuthUser()
424 424 source_repo = self._get_repo(source_repo)
425 425 target_repo = self._get_repo(target_repo)
426 426
427 427 pull_request = PullRequest()
428 428 pull_request.source_repo = source_repo
429 429 pull_request.source_ref = source_ref
430 430 pull_request.target_repo = target_repo
431 431 pull_request.target_ref = target_ref
432 432 pull_request.revisions = revisions
433 433 pull_request.title = title
434 434 pull_request.description = description
435 435 pull_request.description_renderer = description_renderer
436 436 pull_request.author = created_by_user
437 437 pull_request.reviewer_data = reviewer_data
438 438 pull_request.pull_request_state = pull_request.STATE_CREATING
439 439 Session().add(pull_request)
440 440 Session().flush()
441 441
442 442 reviewer_ids = set()
443 443 # members / reviewers
444 444 for reviewer_object in reviewers:
445 445 user_id, reasons, mandatory, rules = reviewer_object
446 446 user = self._get_user(user_id)
447 447
448 448 # skip duplicates
449 449 if user.user_id in reviewer_ids:
450 450 continue
451 451
452 452 reviewer_ids.add(user.user_id)
453 453
454 454 reviewer = PullRequestReviewers()
455 455 reviewer.user = user
456 456 reviewer.pull_request = pull_request
457 457 reviewer.reasons = reasons
458 458 reviewer.mandatory = mandatory
459 459
460 460 # NOTE(marcink): pick only first rule for now
461 461 rule_id = list(rules)[0] if rules else None
462 462 rule = RepoReviewRule.get(rule_id) if rule_id else None
463 463 if rule:
464 464 review_group = rule.user_group_vote_rule(user_id)
465 465 # we check if this particular reviewer is member of a voting group
466 466 if review_group:
467 467 # NOTE(marcink):
468 468 # can be that user is member of more but we pick the first same,
469 469 # same as default reviewers algo
470 470 review_group = review_group[0]
471 471
472 472 rule_data = {
473 473 'rule_name':
474 474 rule.review_rule_name,
475 475 'rule_user_group_entry_id':
476 476 review_group.repo_review_rule_users_group_id,
477 477 'rule_user_group_name':
478 478 review_group.users_group.users_group_name,
479 479 'rule_user_group_members':
480 480 [x.user.username for x in review_group.users_group.members],
481 481 'rule_user_group_members_id':
482 482 [x.user.user_id for x in review_group.users_group.members],
483 483 }
484 484 # e.g {'vote_rule': -1, 'mandatory': True}
485 485 rule_data.update(review_group.rule_data())
486 486
487 487 reviewer.rule_data = rule_data
488 488
489 489 Session().add(reviewer)
490 490 Session().flush()
491 491
492 492 # Set approval status to "Under Review" for all commits which are
493 493 # part of this pull request.
494 494 ChangesetStatusModel().set_status(
495 495 repo=target_repo,
496 496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
497 497 user=created_by_user,
498 498 pull_request=pull_request
499 499 )
500 500 # we commit early at this point. This has to do with a fact
501 501 # that before queries do some row-locking. And because of that
502 502 # we need to commit and finish transaction before below validate call
503 503 # that for large repos could be long resulting in long row locks
504 504 Session().commit()
505 505
506 506 # prepare workspace, and run initial merge simulation. Set state during that
507 507 # operation
508 508 pull_request = PullRequest.get(pull_request.pull_request_id)
509 509
510 510 # set as merging, for simulation, and if finished to created so we mark
511 511 # simulation is working fine
512 512 with pull_request.set_state(PullRequest.STATE_MERGING,
513 513 final_state=PullRequest.STATE_CREATED):
514 514 MergeCheck.validate(
515 515 pull_request, auth_user=auth_user, translator=translator)
516 516
517 517 self.notify_reviewers(pull_request, reviewer_ids)
518 518 self.trigger_pull_request_hook(
519 519 pull_request, created_by_user, 'create')
520 520
521 521 creation_data = pull_request.get_api_data(with_merge_state=False)
522 522 self._log_audit_action(
523 523 'repo.pull_request.create', {'data': creation_data},
524 524 auth_user, pull_request)
525 525
526 526 return pull_request
527 527
528 528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
529 529 pull_request = self.__get_pull_request(pull_request)
530 530 target_scm = pull_request.target_repo.scm_instance()
531 531 if action == 'create':
532 532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
533 533 elif action == 'merge':
534 534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
535 535 elif action == 'close':
536 536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
537 537 elif action == 'review_status_change':
538 538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
539 539 elif action == 'update':
540 540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
541 541 elif action == 'comment':
542 542 # dummy hook ! for comment. We want this function to handle all cases
543 543 def trigger_hook(*args, **kwargs):
544 544 pass
545 545 comment = data['comment']
546 546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
547 547 else:
548 548 return
549 549
550 550 trigger_hook(
551 551 username=user.username,
552 552 repo_name=pull_request.target_repo.repo_name,
553 553 repo_alias=target_scm.alias,
554 554 pull_request=pull_request,
555 555 data=data)
556 556
557 557 def _get_commit_ids(self, pull_request):
558 558 """
559 559 Return the commit ids of the merged pull request.
560 560
561 561 This method is not dealing correctly yet with the lack of autoupdates
562 562 nor with the implicit target updates.
563 563 For example: if a commit in the source repo is already in the target it
564 564 will be reported anyways.
565 565 """
566 566 merge_rev = pull_request.merge_rev
567 567 if merge_rev is None:
568 568 raise ValueError('This pull request was not merged yet')
569 569
570 570 commit_ids = list(pull_request.revisions)
571 571 if merge_rev not in commit_ids:
572 572 commit_ids.append(merge_rev)
573 573
574 574 return commit_ids
575 575
576 576 def merge_repo(self, pull_request, user, extras):
577 577 log.debug("Merging pull request %s", pull_request.pull_request_id)
578 578 extras['user_agent'] = 'internal-merge'
579 579 merge_state = self._merge_pull_request(pull_request, user, extras)
580 580 if merge_state.executed:
581 581 log.debug("Merge was successful, updating the pull request comments.")
582 582 self._comment_and_close_pr(pull_request, user, merge_state)
583 583
584 584 self._log_audit_action(
585 585 'repo.pull_request.merge',
586 586 {'merge_state': merge_state.__dict__},
587 587 user, pull_request)
588 588
589 589 else:
590 590 log.warn("Merge failed, not updating the pull request.")
591 591 return merge_state
592 592
593 593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
594 594 target_vcs = pull_request.target_repo.scm_instance()
595 595 source_vcs = pull_request.source_repo.scm_instance()
596 596
597 597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
598 598 pr_id=pull_request.pull_request_id,
599 599 pr_title=pull_request.title,
600 600 source_repo=source_vcs.name,
601 601 source_ref_name=pull_request.source_ref_parts.name,
602 602 target_repo=target_vcs.name,
603 603 target_ref_name=pull_request.target_ref_parts.name,
604 604 )
605 605
606 606 workspace_id = self._workspace_id(pull_request)
607 607 repo_id = pull_request.target_repo.repo_id
608 608 use_rebase = self._use_rebase_for_merging(pull_request)
609 609 close_branch = self._close_branch_before_merging(pull_request)
610 610
611 611 target_ref = self._refresh_reference(
612 612 pull_request.target_ref_parts, target_vcs)
613 613
614 614 callback_daemon, extras = prepare_callback_daemon(
615 615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 616 host=vcs_settings.HOOKS_HOST,
617 617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
618 618
619 619 with callback_daemon:
620 620 # TODO: johbo: Implement a clean way to run a config_override
621 621 # for a single call.
622 622 target_vcs.config.set(
623 623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
624 624
625 625 user_name = user.short_contact
626 626 merge_state = target_vcs.merge(
627 627 repo_id, workspace_id, target_ref, source_vcs,
628 628 pull_request.source_ref_parts,
629 629 user_name=user_name, user_email=user.email,
630 630 message=message, use_rebase=use_rebase,
631 631 close_branch=close_branch)
632 632 return merge_state
633 633
634 634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
635 635 pull_request.merge_rev = merge_state.merge_ref.commit_id
636 636 pull_request.updated_on = datetime.datetime.now()
637 637 close_msg = close_msg or 'Pull request merged and closed'
638 638
639 639 CommentsModel().create(
640 640 text=safe_unicode(close_msg),
641 641 repo=pull_request.target_repo.repo_id,
642 642 user=user.user_id,
643 643 pull_request=pull_request.pull_request_id,
644 644 f_path=None,
645 645 line_no=None,
646 646 closing_pr=True
647 647 )
648 648
649 649 Session().add(pull_request)
650 650 Session().flush()
651 651 # TODO: paris: replace invalidation with less radical solution
652 652 ScmModel().mark_for_invalidation(
653 653 pull_request.target_repo.repo_name)
654 654 self.trigger_pull_request_hook(pull_request, user, 'merge')
655 655
656 656 def has_valid_update_type(self, pull_request):
657 657 source_ref_type = pull_request.source_ref_parts.type
658 658 return source_ref_type in self.REF_TYPES
659 659
660 660 def update_commits(self, pull_request):
661 661 """
662 662 Get the updated list of commits for the pull request
663 663 and return the new pull request version and the list
664 664 of commits processed by this update action
665 665 """
666 666 pull_request = self.__get_pull_request(pull_request)
667 667 source_ref_type = pull_request.source_ref_parts.type
668 668 source_ref_name = pull_request.source_ref_parts.name
669 669 source_ref_id = pull_request.source_ref_parts.commit_id
670 670
671 671 target_ref_type = pull_request.target_ref_parts.type
672 672 target_ref_name = pull_request.target_ref_parts.name
673 673 target_ref_id = pull_request.target_ref_parts.commit_id
674 674
675 675 if not self.has_valid_update_type(pull_request):
676 676 log.debug("Skipping update of pull request %s due to ref type: %s",
677 677 pull_request, source_ref_type)
678 678 return UpdateResponse(
679 679 executed=False,
680 680 reason=UpdateFailureReason.WRONG_REF_TYPE,
681 681 old=pull_request, new=None, changes=None,
682 682 source_changed=False, target_changed=False)
683 683
684 684 # source repo
685 685 source_repo = pull_request.source_repo.scm_instance()
686 686
687 687 try:
688 688 source_commit = source_repo.get_commit(commit_id=source_ref_name)
689 689 except CommitDoesNotExistError:
690 690 return UpdateResponse(
691 691 executed=False,
692 692 reason=UpdateFailureReason.MISSING_SOURCE_REF,
693 693 old=pull_request, new=None, changes=None,
694 694 source_changed=False, target_changed=False)
695 695
696 696 source_changed = source_ref_id != source_commit.raw_id
697 697
698 698 # target repo
699 699 target_repo = pull_request.target_repo.scm_instance()
700 700
701 701 try:
702 702 target_commit = target_repo.get_commit(commit_id=target_ref_name)
703 703 except CommitDoesNotExistError:
704 704 return UpdateResponse(
705 705 executed=False,
706 706 reason=UpdateFailureReason.MISSING_TARGET_REF,
707 707 old=pull_request, new=None, changes=None,
708 708 source_changed=False, target_changed=False)
709 709 target_changed = target_ref_id != target_commit.raw_id
710 710
711 711 if not (source_changed or target_changed):
712 712 log.debug("Nothing changed in pull request %s", pull_request)
713 713 return UpdateResponse(
714 714 executed=False,
715 715 reason=UpdateFailureReason.NO_CHANGE,
716 716 old=pull_request, new=None, changes=None,
717 717 source_changed=target_changed, target_changed=source_changed)
718 718
719 719 change_in_found = 'target repo' if target_changed else 'source repo'
720 720 log.debug('Updating pull request because of change in %s detected',
721 721 change_in_found)
722 722
723 723 # Finally there is a need for an update, in case of source change
724 724 # we create a new version, else just an update
725 725 if source_changed:
726 726 pull_request_version = self._create_version_from_snapshot(pull_request)
727 727 self._link_comments_to_version(pull_request_version)
728 728 else:
729 729 try:
730 730 ver = pull_request.versions[-1]
731 731 except IndexError:
732 732 ver = None
733 733
734 734 pull_request.pull_request_version_id = \
735 735 ver.pull_request_version_id if ver else None
736 736 pull_request_version = pull_request
737 737
738 738 try:
739 739 if target_ref_type in self.REF_TYPES:
740 740 target_commit = target_repo.get_commit(target_ref_name)
741 741 else:
742 742 target_commit = target_repo.get_commit(target_ref_id)
743 743 except CommitDoesNotExistError:
744 744 return UpdateResponse(
745 745 executed=False,
746 746 reason=UpdateFailureReason.MISSING_TARGET_REF,
747 747 old=pull_request, new=None, changes=None,
748 748 source_changed=source_changed, target_changed=target_changed)
749 749
750 750 # re-compute commit ids
751 751 old_commit_ids = pull_request.revisions
752 752 pre_load = ["author", "branch", "date", "message"]
753 753 commit_ranges = target_repo.compare(
754 754 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
755 755 pre_load=pre_load)
756 756
757 757 ancestor = source_repo.get_common_ancestor(
758 758 source_commit.raw_id, target_commit.raw_id, target_repo)
759 759
760 760 pull_request.source_ref = '%s:%s:%s' % (
761 761 source_ref_type, source_ref_name, source_commit.raw_id)
762 762 pull_request.target_ref = '%s:%s:%s' % (
763 763 target_ref_type, target_ref_name, ancestor)
764 764
765 765 pull_request.revisions = [
766 766 commit.raw_id for commit in reversed(commit_ranges)]
767 767 pull_request.updated_on = datetime.datetime.now()
768 768 Session().add(pull_request)
769 769 new_commit_ids = pull_request.revisions
770 770
771 771 old_diff_data, new_diff_data = self._generate_update_diffs(
772 772 pull_request, pull_request_version)
773 773
774 774 # calculate commit and file changes
775 775 changes = self._calculate_commit_id_changes(
776 776 old_commit_ids, new_commit_ids)
777 777 file_changes = self._calculate_file_changes(
778 778 old_diff_data, new_diff_data)
779 779
780 780 # set comments as outdated if DIFFS changed
781 781 CommentsModel().outdate_comments(
782 782 pull_request, old_diff_data=old_diff_data,
783 783 new_diff_data=new_diff_data)
784 784
785 785 commit_changes = (changes.added or changes.removed)
786 786 file_node_changes = (
787 787 file_changes.added or file_changes.modified or file_changes.removed)
788 788 pr_has_changes = commit_changes or file_node_changes
789 789
790 790 # Add an automatic comment to the pull request, in case
791 791 # anything has changed
792 792 if pr_has_changes:
793 793 update_comment = CommentsModel().create(
794 794 text=self._render_update_message(changes, file_changes),
795 795 repo=pull_request.target_repo,
796 796 user=pull_request.author,
797 797 pull_request=pull_request,
798 798 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
799 799
800 800 # Update status to "Under Review" for added commits
801 801 for commit_id in changes.added:
802 802 ChangesetStatusModel().set_status(
803 803 repo=pull_request.source_repo,
804 804 status=ChangesetStatus.STATUS_UNDER_REVIEW,
805 805 comment=update_comment,
806 806 user=pull_request.author,
807 807 pull_request=pull_request,
808 808 revision=commit_id)
809 809
810 810 log.debug(
811 811 'Updated pull request %s, added_ids: %s, common_ids: %s, '
812 812 'removed_ids: %s', pull_request.pull_request_id,
813 813 changes.added, changes.common, changes.removed)
814 814 log.debug(
815 815 'Updated pull request with the following file changes: %s',
816 816 file_changes)
817 817
818 818 log.info(
819 819 "Updated pull request %s from commit %s to commit %s, "
820 820 "stored new version %s of this pull request.",
821 821 pull_request.pull_request_id, source_ref_id,
822 822 pull_request.source_ref_parts.commit_id,
823 823 pull_request_version.pull_request_version_id)
824 824 Session().commit()
825 825 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
826 826
827 827 return UpdateResponse(
828 828 executed=True, reason=UpdateFailureReason.NONE,
829 829 old=pull_request, new=pull_request_version, changes=changes,
830 830 source_changed=source_changed, target_changed=target_changed)
831 831
832 832 def _create_version_from_snapshot(self, pull_request):
833 833 version = PullRequestVersion()
834 834 version.title = pull_request.title
835 835 version.description = pull_request.description
836 836 version.status = pull_request.status
837 837 version.pull_request_state = pull_request.pull_request_state
838 838 version.created_on = datetime.datetime.now()
839 839 version.updated_on = pull_request.updated_on
840 840 version.user_id = pull_request.user_id
841 841 version.source_repo = pull_request.source_repo
842 842 version.source_ref = pull_request.source_ref
843 843 version.target_repo = pull_request.target_repo
844 844 version.target_ref = pull_request.target_ref
845 845
846 846 version._last_merge_source_rev = pull_request._last_merge_source_rev
847 847 version._last_merge_target_rev = pull_request._last_merge_target_rev
848 848 version.last_merge_status = pull_request.last_merge_status
849 849 version.shadow_merge_ref = pull_request.shadow_merge_ref
850 850 version.merge_rev = pull_request.merge_rev
851 851 version.reviewer_data = pull_request.reviewer_data
852 852
853 853 version.revisions = pull_request.revisions
854 854 version.pull_request = pull_request
855 855 Session().add(version)
856 856 Session().flush()
857 857
858 858 return version
859 859
860 860 def _generate_update_diffs(self, pull_request, pull_request_version):
861 861
862 862 diff_context = (
863 863 self.DIFF_CONTEXT +
864 864 CommentsModel.needed_extra_diff_context())
865 865 hide_whitespace_changes = False
866 866 source_repo = pull_request_version.source_repo
867 867 source_ref_id = pull_request_version.source_ref_parts.commit_id
868 868 target_ref_id = pull_request_version.target_ref_parts.commit_id
869 869 old_diff = self._get_diff_from_pr_or_version(
870 870 source_repo, source_ref_id, target_ref_id,
871 871 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
872 872
873 873 source_repo = pull_request.source_repo
874 874 source_ref_id = pull_request.source_ref_parts.commit_id
875 875 target_ref_id = pull_request.target_ref_parts.commit_id
876 876
877 877 new_diff = self._get_diff_from_pr_or_version(
878 878 source_repo, source_ref_id, target_ref_id,
879 879 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
880 880
881 881 old_diff_data = diffs.DiffProcessor(old_diff)
882 882 old_diff_data.prepare()
883 883 new_diff_data = diffs.DiffProcessor(new_diff)
884 884 new_diff_data.prepare()
885 885
886 886 return old_diff_data, new_diff_data
887 887
888 888 def _link_comments_to_version(self, pull_request_version):
889 889 """
890 890 Link all unlinked comments of this pull request to the given version.
891 891
892 892 :param pull_request_version: The `PullRequestVersion` to which
893 893 the comments shall be linked.
894 894
895 895 """
896 896 pull_request = pull_request_version.pull_request
897 897 comments = ChangesetComment.query()\
898 898 .filter(
899 899 # TODO: johbo: Should we query for the repo at all here?
900 900 # Pending decision on how comments of PRs are to be related
901 901 # to either the source repo, the target repo or no repo at all.
902 902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
903 903 ChangesetComment.pull_request == pull_request,
904 904 ChangesetComment.pull_request_version == None)\
905 905 .order_by(ChangesetComment.comment_id.asc())
906 906
907 907 # TODO: johbo: Find out why this breaks if it is done in a bulk
908 908 # operation.
909 909 for comment in comments:
910 910 comment.pull_request_version_id = (
911 911 pull_request_version.pull_request_version_id)
912 912 Session().add(comment)
913 913
914 914 def _calculate_commit_id_changes(self, old_ids, new_ids):
915 915 added = [x for x in new_ids if x not in old_ids]
916 916 common = [x for x in new_ids if x in old_ids]
917 917 removed = [x for x in old_ids if x not in new_ids]
918 918 total = new_ids
919 919 return ChangeTuple(added, common, removed, total)
920 920
921 921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
922 922
923 923 old_files = OrderedDict()
924 924 for diff_data in old_diff_data.parsed_diff:
925 925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
926 926
927 927 added_files = []
928 928 modified_files = []
929 929 removed_files = []
930 930 for diff_data in new_diff_data.parsed_diff:
931 931 new_filename = diff_data['filename']
932 932 new_hash = md5_safe(diff_data['raw_diff'])
933 933
934 934 old_hash = old_files.get(new_filename)
935 935 if not old_hash:
936 936 # file is not present in old diff, means it's added
937 937 added_files.append(new_filename)
938 938 else:
939 939 if new_hash != old_hash:
940 940 modified_files.append(new_filename)
941 941 # now remove a file from old, since we have seen it already
942 942 del old_files[new_filename]
943 943
944 944 # removed files is when there are present in old, but not in NEW,
945 945 # since we remove old files that are present in new diff, left-overs
946 946 # if any should be the removed files
947 947 removed_files.extend(old_files.keys())
948 948
949 949 return FileChangeTuple(added_files, modified_files, removed_files)
950 950
951 951 def _render_update_message(self, changes, file_changes):
952 952 """
953 953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
954 954 so it's always looking the same disregarding on which default
955 955 renderer system is using.
956 956
957 957 :param changes: changes named tuple
958 958 :param file_changes: file changes named tuple
959 959
960 960 """
961 961 new_status = ChangesetStatus.get_status_lbl(
962 962 ChangesetStatus.STATUS_UNDER_REVIEW)
963 963
964 964 changed_files = (
965 965 file_changes.added + file_changes.modified + file_changes.removed)
966 966
967 967 params = {
968 968 'under_review_label': new_status,
969 969 'added_commits': changes.added,
970 970 'removed_commits': changes.removed,
971 971 'changed_files': changed_files,
972 972 'added_files': file_changes.added,
973 973 'modified_files': file_changes.modified,
974 974 'removed_files': file_changes.removed,
975 975 }
976 976 renderer = RstTemplateRenderer()
977 977 return renderer.render('pull_request_update.mako', **params)
978 978
979 979 def edit(self, pull_request, title, description, description_renderer, user):
980 980 pull_request = self.__get_pull_request(pull_request)
981 981 old_data = pull_request.get_api_data(with_merge_state=False)
982 982 if pull_request.is_closed():
983 983 raise ValueError('This pull request is closed')
984 984 if title:
985 985 pull_request.title = title
986 986 pull_request.description = description
987 987 pull_request.updated_on = datetime.datetime.now()
988 988 pull_request.description_renderer = description_renderer
989 989 Session().add(pull_request)
990 990 self._log_audit_action(
991 991 'repo.pull_request.edit', {'old_data': old_data},
992 992 user, pull_request)
993 993
994 994 def update_reviewers(self, pull_request, reviewer_data, user):
995 995 """
996 996 Update the reviewers in the pull request
997 997
998 998 :param pull_request: the pr to update
999 999 :param reviewer_data: list of tuples
1000 1000 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1001 1001 """
1002 1002 pull_request = self.__get_pull_request(pull_request)
1003 1003 if pull_request.is_closed():
1004 1004 raise ValueError('This pull request is closed')
1005 1005
1006 1006 reviewers = {}
1007 1007 for user_id, reasons, mandatory, rules in reviewer_data:
1008 1008 if isinstance(user_id, (int, compat.string_types)):
1009 1009 user_id = self._get_user(user_id).user_id
1010 1010 reviewers[user_id] = {
1011 1011 'reasons': reasons, 'mandatory': mandatory}
1012 1012
1013 1013 reviewers_ids = set(reviewers.keys())
1014 1014 current_reviewers = PullRequestReviewers.query()\
1015 1015 .filter(PullRequestReviewers.pull_request ==
1016 1016 pull_request).all()
1017 1017 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1018 1018
1019 1019 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1020 1020 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1021 1021
1022 1022 log.debug("Adding %s reviewers", ids_to_add)
1023 1023 log.debug("Removing %s reviewers", ids_to_remove)
1024 1024 changed = False
1025 1025 added_audit_reviewers = []
1026 1026 removed_audit_reviewers = []
1027 1027
1028 1028 for uid in ids_to_add:
1029 1029 changed = True
1030 1030 _usr = self._get_user(uid)
1031 1031 reviewer = PullRequestReviewers()
1032 1032 reviewer.user = _usr
1033 1033 reviewer.pull_request = pull_request
1034 1034 reviewer.reasons = reviewers[uid]['reasons']
1035 1035 # NOTE(marcink): mandatory shouldn't be changed now
1036 1036 # reviewer.mandatory = reviewers[uid]['reasons']
1037 1037 Session().add(reviewer)
1038 1038 added_audit_reviewers.append(reviewer.get_dict())
1039 1039
1040 1040 for uid in ids_to_remove:
1041 1041 changed = True
1042 1042 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1043 1043 # that prevents and fixes cases that we added the same reviewer twice.
1044 1044 # this CAN happen due to the lack of DB checks
1045 1045 reviewers = PullRequestReviewers.query()\
1046 1046 .filter(PullRequestReviewers.user_id == uid,
1047 1047 PullRequestReviewers.pull_request == pull_request)\
1048 1048 .all()
1049 1049
1050 1050 for obj in reviewers:
1051 1051 added_audit_reviewers.append(obj.get_dict())
1052 1052 Session().delete(obj)
1053 1053
1054 1054 if changed:
1055 1055 Session().expire_all()
1056 1056 pull_request.updated_on = datetime.datetime.now()
1057 1057 Session().add(pull_request)
1058 1058
1059 1059 # finally store audit logs
1060 1060 for user_data in added_audit_reviewers:
1061 1061 self._log_audit_action(
1062 1062 'repo.pull_request.reviewer.add', {'data': user_data},
1063 1063 user, pull_request)
1064 1064 for user_data in removed_audit_reviewers:
1065 1065 self._log_audit_action(
1066 1066 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1067 1067 user, pull_request)
1068 1068
1069 1069 self.notify_reviewers(pull_request, ids_to_add)
1070 1070 return ids_to_add, ids_to_remove
1071 1071
1072 1072 def get_url(self, pull_request, request=None, permalink=False):
1073 1073 if not request:
1074 1074 request = get_current_request()
1075 1075
1076 1076 if permalink:
1077 1077 return request.route_url(
1078 1078 'pull_requests_global',
1079 1079 pull_request_id=pull_request.pull_request_id,)
1080 1080 else:
1081 1081 return request.route_url('pullrequest_show',
1082 1082 repo_name=safe_str(pull_request.target_repo.repo_name),
1083 1083 pull_request_id=pull_request.pull_request_id,)
1084 1084
1085 1085 def get_shadow_clone_url(self, pull_request, request=None):
1086 1086 """
1087 1087 Returns qualified url pointing to the shadow repository. If this pull
1088 1088 request is closed there is no shadow repository and ``None`` will be
1089 1089 returned.
1090 1090 """
1091 1091 if pull_request.is_closed():
1092 1092 return None
1093 1093 else:
1094 1094 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1095 1095 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1096 1096
1097 1097 def notify_reviewers(self, pull_request, reviewers_ids):
1098 1098 # notification to reviewers
1099 1099 if not reviewers_ids:
1100 1100 return
1101 1101
1102 1102 pull_request_obj = pull_request
1103 1103 # get the current participants of this pull request
1104 1104 recipients = reviewers_ids
1105 1105 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1106 1106
1107 1107 pr_source_repo = pull_request_obj.source_repo
1108 1108 pr_target_repo = pull_request_obj.target_repo
1109 1109
1110 1110 pr_url = h.route_url('pullrequest_show',
1111 1111 repo_name=pr_target_repo.repo_name,
1112 1112 pull_request_id=pull_request_obj.pull_request_id,)
1113 1113
1114 1114 # set some variables for email notification
1115 1115 pr_target_repo_url = h.route_url(
1116 1116 'repo_summary', repo_name=pr_target_repo.repo_name)
1117 1117
1118 1118 pr_source_repo_url = h.route_url(
1119 1119 'repo_summary', repo_name=pr_source_repo.repo_name)
1120 1120
1121 1121 # pull request specifics
1122 1122 pull_request_commits = [
1123 1123 (x.raw_id, x.message)
1124 1124 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1125 1125
1126 1126 kwargs = {
1127 1127 'user': pull_request.author,
1128 1128 'pull_request': pull_request_obj,
1129 1129 'pull_request_commits': pull_request_commits,
1130 1130
1131 1131 'pull_request_target_repo': pr_target_repo,
1132 1132 'pull_request_target_repo_url': pr_target_repo_url,
1133 1133
1134 1134 'pull_request_source_repo': pr_source_repo,
1135 1135 'pull_request_source_repo_url': pr_source_repo_url,
1136 1136
1137 1137 'pull_request_url': pr_url,
1138 1138 }
1139 1139
1140 1140 # pre-generate the subject for notification itself
1141 1141 (subject,
1142 1142 _h, _e, # we don't care about those
1143 1143 body_plaintext) = EmailNotificationModel().render_email(
1144 1144 notification_type, **kwargs)
1145 1145
1146 1146 # create notification objects, and emails
1147 1147 NotificationModel().create(
1148 1148 created_by=pull_request.author,
1149 1149 notification_subject=subject,
1150 1150 notification_body=body_plaintext,
1151 1151 notification_type=notification_type,
1152 1152 recipients=recipients,
1153 1153 email_kwargs=kwargs,
1154 1154 )
1155 1155
1156 1156 def delete(self, pull_request, user):
1157 1157 pull_request = self.__get_pull_request(pull_request)
1158 1158 old_data = pull_request.get_api_data(with_merge_state=False)
1159 1159 self._cleanup_merge_workspace(pull_request)
1160 1160 self._log_audit_action(
1161 1161 'repo.pull_request.delete', {'old_data': old_data},
1162 1162 user, pull_request)
1163 1163 Session().delete(pull_request)
1164 1164
1165 1165 def close_pull_request(self, pull_request, user):
1166 1166 pull_request = self.__get_pull_request(pull_request)
1167 1167 self._cleanup_merge_workspace(pull_request)
1168 1168 pull_request.status = PullRequest.STATUS_CLOSED
1169 1169 pull_request.updated_on = datetime.datetime.now()
1170 1170 Session().add(pull_request)
1171 1171 self.trigger_pull_request_hook(
1172 1172 pull_request, pull_request.author, 'close')
1173 1173
1174 1174 pr_data = pull_request.get_api_data(with_merge_state=False)
1175 1175 self._log_audit_action(
1176 1176 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1177 1177
1178 1178 def close_pull_request_with_comment(
1179 1179 self, pull_request, user, repo, message=None, auth_user=None):
1180 1180
1181 1181 pull_request_review_status = pull_request.calculated_review_status()
1182 1182
1183 1183 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1184 1184 # approved only if we have voting consent
1185 1185 status = ChangesetStatus.STATUS_APPROVED
1186 1186 else:
1187 1187 status = ChangesetStatus.STATUS_REJECTED
1188 1188 status_lbl = ChangesetStatus.get_status_lbl(status)
1189 1189
1190 1190 default_message = (
1191 1191 'Closing with status change {transition_icon} {status}.'
1192 1192 ).format(transition_icon='>', status=status_lbl)
1193 1193 text = message or default_message
1194 1194
1195 1195 # create a comment, and link it to new status
1196 1196 comment = CommentsModel().create(
1197 1197 text=text,
1198 1198 repo=repo.repo_id,
1199 1199 user=user.user_id,
1200 1200 pull_request=pull_request.pull_request_id,
1201 1201 status_change=status_lbl,
1202 1202 status_change_type=status,
1203 1203 closing_pr=True,
1204 1204 auth_user=auth_user,
1205 1205 )
1206 1206
1207 1207 # calculate old status before we change it
1208 1208 old_calculated_status = pull_request.calculated_review_status()
1209 1209 ChangesetStatusModel().set_status(
1210 1210 repo.repo_id,
1211 1211 status,
1212 1212 user.user_id,
1213 1213 comment=comment,
1214 1214 pull_request=pull_request.pull_request_id
1215 1215 )
1216 1216
1217 1217 Session().flush()
1218 1218 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1219 1219 # we now calculate the status of pull request again, and based on that
1220 1220 # calculation trigger status change. This might happen in cases
1221 1221 # that non-reviewer admin closes a pr, which means his vote doesn't
1222 1222 # change the status, while if he's a reviewer this might change it.
1223 1223 calculated_status = pull_request.calculated_review_status()
1224 1224 if old_calculated_status != calculated_status:
1225 1225 self.trigger_pull_request_hook(
1226 1226 pull_request, user, 'review_status_change',
1227 1227 data={'status': calculated_status})
1228 1228
1229 1229 # finally close the PR
1230 1230 PullRequestModel().close_pull_request(
1231 1231 pull_request.pull_request_id, user)
1232 1232
1233 1233 return comment, status
1234 1234
1235 1235 def merge_status(self, pull_request, translator=None,
1236 1236 force_shadow_repo_refresh=False):
1237 1237 _ = translator or get_current_request().translate
1238 1238
1239 1239 if not self._is_merge_enabled(pull_request):
1240 1240 return False, _('Server-side pull request merging is disabled.')
1241 1241 if pull_request.is_closed():
1242 1242 return False, _('This pull request is closed.')
1243 1243 merge_possible, msg = self._check_repo_requirements(
1244 1244 target=pull_request.target_repo, source=pull_request.source_repo,
1245 1245 translator=_)
1246 1246 if not merge_possible:
1247 1247 return merge_possible, msg
1248 1248
1249 1249 try:
1250 1250 resp = self._try_merge(
1251 1251 pull_request,
1252 1252 force_shadow_repo_refresh=force_shadow_repo_refresh)
1253 1253 log.debug("Merge response: %s", resp)
1254 1254 status = resp.possible, resp.merge_status_message
1255 1255 except NotImplementedError:
1256 1256 status = False, _('Pull request merging is not supported.')
1257 1257
1258 1258 return status
1259 1259
1260 1260 def _check_repo_requirements(self, target, source, translator):
1261 1261 """
1262 1262 Check if `target` and `source` have compatible requirements.
1263 1263
1264 1264 Currently this is just checking for largefiles.
1265 1265 """
1266 1266 _ = translator
1267 1267 target_has_largefiles = self._has_largefiles(target)
1268 1268 source_has_largefiles = self._has_largefiles(source)
1269 1269 merge_possible = True
1270 1270 message = u''
1271 1271
1272 1272 if target_has_largefiles != source_has_largefiles:
1273 1273 merge_possible = False
1274 1274 if source_has_largefiles:
1275 1275 message = _(
1276 1276 'Target repository large files support is disabled.')
1277 1277 else:
1278 1278 message = _(
1279 1279 'Source repository large files support is disabled.')
1280 1280
1281 1281 return merge_possible, message
1282 1282
1283 1283 def _has_largefiles(self, repo):
1284 1284 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1285 1285 'extensions', 'largefiles')
1286 1286 return largefiles_ui and largefiles_ui[0].active
1287 1287
1288 1288 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1289 1289 """
1290 1290 Try to merge the pull request and return the merge status.
1291 1291 """
1292 1292 log.debug(
1293 1293 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1294 1294 pull_request.pull_request_id, force_shadow_repo_refresh)
1295 1295 target_vcs = pull_request.target_repo.scm_instance()
1296 1296 # Refresh the target reference.
1297 1297 try:
1298 1298 target_ref = self._refresh_reference(
1299 1299 pull_request.target_ref_parts, target_vcs)
1300 1300 except CommitDoesNotExistError:
1301 1301 merge_state = MergeResponse(
1302 1302 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1303 1303 metadata={'target_ref': pull_request.target_ref_parts})
1304 1304 return merge_state
1305 1305
1306 1306 target_locked = pull_request.target_repo.locked
1307 1307 if target_locked and target_locked[0]:
1308 1308 locked_by = 'user:{}'.format(target_locked[0])
1309 1309 log.debug("The target repository is locked by %s.", locked_by)
1310 1310 merge_state = MergeResponse(
1311 1311 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1312 1312 metadata={'locked_by': locked_by})
1313 1313 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1314 1314 pull_request, target_ref):
1315 1315 log.debug("Refreshing the merge status of the repository.")
1316 1316 merge_state = self._refresh_merge_state(
1317 1317 pull_request, target_vcs, target_ref)
1318 1318 else:
1319 1319 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1320 1320 metadata = {
1321 1321 'target_ref': pull_request.target_ref_parts,
1322 1322 'source_ref': pull_request.source_ref_parts,
1323 1323 }
1324 1324 if not possible and target_ref.type == 'branch':
1325 1325 # NOTE(marcink): case for mercurial multiple heads on branch
1326 1326 heads = target_vcs._heads(target_ref.name)
1327 1327 if len(heads) != 1:
1328 1328 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1329 1329 metadata.update({
1330 1330 'heads': heads
1331 1331 })
1332 1332 merge_state = MergeResponse(
1333 1333 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1334 1334
1335 1335 return merge_state
1336 1336
1337 1337 def _refresh_reference(self, reference, vcs_repository):
1338 1338 if reference.type in self.UPDATABLE_REF_TYPES:
1339 1339 name_or_id = reference.name
1340 1340 else:
1341 1341 name_or_id = reference.commit_id
1342 1342
1343 1343 refreshed_commit = vcs_repository.get_commit(name_or_id)
1344 1344 refreshed_reference = Reference(
1345 1345 reference.type, reference.name, refreshed_commit.raw_id)
1346 1346 return refreshed_reference
1347 1347
1348 1348 def _needs_merge_state_refresh(self, pull_request, target_reference):
1349 1349 return not(
1350 1350 pull_request.revisions and
1351 1351 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1352 1352 target_reference.commit_id == pull_request._last_merge_target_rev)
1353 1353
1354 1354 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1355 1355 workspace_id = self._workspace_id(pull_request)
1356 1356 source_vcs = pull_request.source_repo.scm_instance()
1357 1357 repo_id = pull_request.target_repo.repo_id
1358 1358 use_rebase = self._use_rebase_for_merging(pull_request)
1359 1359 close_branch = self._close_branch_before_merging(pull_request)
1360 1360 merge_state = target_vcs.merge(
1361 1361 repo_id, workspace_id,
1362 1362 target_reference, source_vcs, pull_request.source_ref_parts,
1363 1363 dry_run=True, use_rebase=use_rebase,
1364 1364 close_branch=close_branch)
1365 1365
1366 1366 # Do not store the response if there was an unknown error.
1367 1367 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1368 1368 pull_request._last_merge_source_rev = \
1369 1369 pull_request.source_ref_parts.commit_id
1370 1370 pull_request._last_merge_target_rev = target_reference.commit_id
1371 1371 pull_request.last_merge_status = merge_state.failure_reason
1372 1372 pull_request.shadow_merge_ref = merge_state.merge_ref
1373 1373 Session().add(pull_request)
1374 1374 Session().commit()
1375 1375
1376 1376 return merge_state
1377 1377
1378 1378 def _workspace_id(self, pull_request):
1379 1379 workspace_id = 'pr-%s' % pull_request.pull_request_id
1380 1380 return workspace_id
1381 1381
1382 1382 def generate_repo_data(self, repo, commit_id=None, branch=None,
1383 1383 bookmark=None, translator=None):
1384 1384 from rhodecode.model.repo import RepoModel
1385 1385
1386 1386 all_refs, selected_ref = \
1387 1387 self._get_repo_pullrequest_sources(
1388 1388 repo.scm_instance(), commit_id=commit_id,
1389 1389 branch=branch, bookmark=bookmark, translator=translator)
1390 1390
1391 1391 refs_select2 = []
1392 1392 for element in all_refs:
1393 1393 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1394 1394 refs_select2.append({'text': element[1], 'children': children})
1395 1395
1396 1396 return {
1397 1397 'user': {
1398 1398 'user_id': repo.user.user_id,
1399 1399 'username': repo.user.username,
1400 1400 'firstname': repo.user.first_name,
1401 1401 'lastname': repo.user.last_name,
1402 1402 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1403 1403 },
1404 1404 'name': repo.repo_name,
1405 1405 'link': RepoModel().get_url(repo),
1406 1406 'description': h.chop_at_smart(repo.description_safe, '\n'),
1407 1407 'refs': {
1408 1408 'all_refs': all_refs,
1409 1409 'selected_ref': selected_ref,
1410 1410 'select2_refs': refs_select2
1411 1411 }
1412 1412 }
1413 1413
1414 1414 def generate_pullrequest_title(self, source, source_ref, target):
1415 1415 return u'{source}#{at_ref} to {target}'.format(
1416 1416 source=source,
1417 1417 at_ref=source_ref,
1418 1418 target=target,
1419 1419 )
1420 1420
1421 1421 def _cleanup_merge_workspace(self, pull_request):
1422 1422 # Merging related cleanup
1423 1423 repo_id = pull_request.target_repo.repo_id
1424 1424 target_scm = pull_request.target_repo.scm_instance()
1425 1425 workspace_id = self._workspace_id(pull_request)
1426 1426
1427 1427 try:
1428 1428 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1429 1429 except NotImplementedError:
1430 1430 pass
1431 1431
1432 1432 def _get_repo_pullrequest_sources(
1433 1433 self, repo, commit_id=None, branch=None, bookmark=None,
1434 1434 translator=None):
1435 1435 """
1436 1436 Return a structure with repo's interesting commits, suitable for
1437 1437 the selectors in pullrequest controller
1438 1438
1439 1439 :param commit_id: a commit that must be in the list somehow
1440 1440 and selected by default
1441 1441 :param branch: a branch that must be in the list and selected
1442 1442 by default - even if closed
1443 1443 :param bookmark: a bookmark that must be in the list and selected
1444 1444 """
1445 1445 _ = translator or get_current_request().translate
1446 1446
1447 1447 commit_id = safe_str(commit_id) if commit_id else None
1448 1448 branch = safe_unicode(branch) if branch else None
1449 1449 bookmark = safe_unicode(bookmark) if bookmark else None
1450 1450
1451 1451 selected = None
1452 1452
1453 1453 # order matters: first source that has commit_id in it will be selected
1454 1454 sources = []
1455 1455 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1456 1456 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1457 1457
1458 1458 if commit_id:
1459 1459 ref_commit = (h.short_id(commit_id), commit_id)
1460 1460 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1461 1461
1462 1462 sources.append(
1463 1463 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1464 1464 )
1465 1465
1466 1466 groups = []
1467 1467
1468 1468 for group_key, ref_list, group_name, match in sources:
1469 1469 group_refs = []
1470 1470 for ref_name, ref_id in ref_list:
1471 1471 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1472 1472 group_refs.append((ref_key, ref_name))
1473 1473
1474 1474 if not selected:
1475 1475 if set([commit_id, match]) & set([ref_id, ref_name]):
1476 1476 selected = ref_key
1477 1477
1478 1478 if group_refs:
1479 1479 groups.append((group_refs, group_name))
1480 1480
1481 1481 if not selected:
1482 1482 ref = commit_id or branch or bookmark
1483 1483 if ref:
1484 1484 raise CommitDoesNotExistError(
1485 1485 u'No commit refs could be found matching: {}'.format(ref))
1486 1486 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1487 1487 selected = u'branch:{}:{}'.format(
1488 1488 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1489 1489 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1490 1490 )
1491 1491 elif repo.commit_ids:
1492 1492 # make the user select in this case
1493 1493 selected = None
1494 1494 else:
1495 1495 raise EmptyRepositoryError()
1496 1496 return groups, selected
1497 1497
1498 1498 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1499 1499 hide_whitespace_changes, diff_context):
1500 1500
1501 1501 return self._get_diff_from_pr_or_version(
1502 1502 source_repo, source_ref_id, target_ref_id,
1503 1503 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1504 1504
1505 1505 def _get_diff_from_pr_or_version(
1506 1506 self, source_repo, source_ref_id, target_ref_id,
1507 1507 hide_whitespace_changes, diff_context):
1508 1508
1509 1509 target_commit = source_repo.get_commit(
1510 1510 commit_id=safe_str(target_ref_id))
1511 1511 source_commit = source_repo.get_commit(
1512 1512 commit_id=safe_str(source_ref_id))
1513 1513 if isinstance(source_repo, Repository):
1514 1514 vcs_repo = source_repo.scm_instance()
1515 1515 else:
1516 1516 vcs_repo = source_repo
1517 1517
1518 1518 # TODO: johbo: In the context of an update, we cannot reach
1519 1519 # the old commit anymore with our normal mechanisms. It needs
1520 1520 # some sort of special support in the vcs layer to avoid this
1521 1521 # workaround.
1522 1522 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1523 1523 vcs_repo.alias == 'git'):
1524 1524 source_commit.raw_id = safe_str(source_ref_id)
1525 1525
1526 1526 log.debug('calculating diff between '
1527 1527 'source_ref:%s and target_ref:%s for repo `%s`',
1528 1528 target_ref_id, source_ref_id,
1529 1529 safe_unicode(vcs_repo.path))
1530 1530
1531 1531 vcs_diff = vcs_repo.get_diff(
1532 1532 commit1=target_commit, commit2=source_commit,
1533 1533 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1534 1534 return vcs_diff
1535 1535
1536 1536 def _is_merge_enabled(self, pull_request):
1537 1537 return self._get_general_setting(
1538 1538 pull_request, 'rhodecode_pr_merge_enabled')
1539 1539
1540 1540 def _use_rebase_for_merging(self, pull_request):
1541 1541 repo_type = pull_request.target_repo.repo_type
1542 1542 if repo_type == 'hg':
1543 1543 return self._get_general_setting(
1544 1544 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1545 1545 elif repo_type == 'git':
1546 1546 return self._get_general_setting(
1547 1547 pull_request, 'rhodecode_git_use_rebase_for_merging')
1548 1548
1549 1549 return False
1550 1550
1551 1551 def _close_branch_before_merging(self, pull_request):
1552 1552 repo_type = pull_request.target_repo.repo_type
1553 1553 if repo_type == 'hg':
1554 1554 return self._get_general_setting(
1555 1555 pull_request, 'rhodecode_hg_close_branch_before_merging')
1556 1556 elif repo_type == 'git':
1557 1557 return self._get_general_setting(
1558 1558 pull_request, 'rhodecode_git_close_branch_before_merging')
1559 1559
1560 1560 return False
1561 1561
1562 1562 def _get_general_setting(self, pull_request, settings_key, default=False):
1563 1563 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1564 1564 settings = settings_model.get_general_settings()
1565 1565 return settings.get(settings_key, default)
1566 1566
1567 1567 def _log_audit_action(self, action, action_data, user, pull_request):
1568 1568 audit_logger.store(
1569 1569 action=action,
1570 1570 action_data=action_data,
1571 1571 user=user,
1572 1572 repo=pull_request.target_repo)
1573 1573
1574 1574 def get_reviewer_functions(self):
1575 1575 """
1576 1576 Fetches functions for validation and fetching default reviewers.
1577 1577 If available we use the EE package, else we fallback to CE
1578 1578 package functions
1579 1579 """
1580 1580 try:
1581 1581 from rc_reviewers.utils import get_default_reviewers_data
1582 1582 from rc_reviewers.utils import validate_default_reviewers
1583 1583 except ImportError:
1584 1584 from rhodecode.apps.repository.utils import get_default_reviewers_data
1585 1585 from rhodecode.apps.repository.utils import validate_default_reviewers
1586 1586
1587 1587 return get_default_reviewers_data, validate_default_reviewers
1588 1588
1589 1589
1590 1590 class MergeCheck(object):
1591 1591 """
1592 1592 Perform Merge Checks and returns a check object which stores information
1593 1593 about merge errors, and merge conditions
1594 1594 """
1595 1595 TODO_CHECK = 'todo'
1596 1596 PERM_CHECK = 'perm'
1597 1597 REVIEW_CHECK = 'review'
1598 1598 MERGE_CHECK = 'merge'
1599 1599
1600 1600 def __init__(self):
1601 1601 self.review_status = None
1602 1602 self.merge_possible = None
1603 1603 self.merge_msg = ''
1604 1604 self.failed = None
1605 1605 self.errors = []
1606 1606 self.error_details = OrderedDict()
1607 1607
1608 1608 def push_error(self, error_type, message, error_key, details):
1609 1609 self.failed = True
1610 1610 self.errors.append([error_type, message])
1611 1611 self.error_details[error_key] = dict(
1612 1612 details=details,
1613 1613 error_type=error_type,
1614 1614 message=message
1615 1615 )
1616 1616
1617 1617 @classmethod
1618 1618 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1619 1619 force_shadow_repo_refresh=False):
1620 1620 _ = translator
1621 1621 merge_check = cls()
1622 1622
1623 1623 # permissions to merge
1624 1624 user_allowed_to_merge = PullRequestModel().check_user_merge(
1625 1625 pull_request, auth_user)
1626 1626 if not user_allowed_to_merge:
1627 1627 log.debug("MergeCheck: cannot merge, approval is pending.")
1628 1628
1629 1629 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1630 1630 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1631 1631 if fail_early:
1632 1632 return merge_check
1633 1633
1634 1634 # permission to merge into the target branch
1635 1635 target_commit_id = pull_request.target_ref_parts.commit_id
1636 1636 if pull_request.target_ref_parts.type == 'branch':
1637 1637 branch_name = pull_request.target_ref_parts.name
1638 1638 else:
1639 1639 # for mercurial we can always figure out the branch from the commit
1640 1640 # in case of bookmark
1641 1641 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1642 1642 branch_name = target_commit.branch
1643 1643
1644 1644 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1645 1645 pull_request.target_repo.repo_name, branch_name)
1646 1646 if branch_perm and branch_perm == 'branch.none':
1647 1647 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1648 1648 branch_name, rule)
1649 1649 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1650 1650 if fail_early:
1651 1651 return merge_check
1652 1652
1653 1653 # review status, must be always present
1654 1654 review_status = pull_request.calculated_review_status()
1655 1655 merge_check.review_status = review_status
1656 1656
1657 1657 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1658 1658 if not status_approved:
1659 1659 log.debug("MergeCheck: cannot merge, approval is pending.")
1660 1660
1661 1661 msg = _('Pull request reviewer approval is pending.')
1662 1662
1663 1663 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1664 1664
1665 1665 if fail_early:
1666 1666 return merge_check
1667 1667
1668 1668 # left over TODOs
1669 1669 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1670 1670 if todos:
1671 1671 log.debug("MergeCheck: cannot merge, {} "
1672 1672 "unresolved TODOs left.".format(len(todos)))
1673 1673
1674 1674 if len(todos) == 1:
1675 1675 msg = _('Cannot merge, {} TODO still not resolved.').format(
1676 1676 len(todos))
1677 1677 else:
1678 1678 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1679 1679 len(todos))
1680 1680
1681 1681 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1682 1682
1683 1683 if fail_early:
1684 1684 return merge_check
1685 1685
1686 1686 # merge possible, here is the filesystem simulation + shadow repo
1687 1687 merge_status, msg = PullRequestModel().merge_status(
1688 1688 pull_request, translator=translator,
1689 1689 force_shadow_repo_refresh=force_shadow_repo_refresh)
1690 1690 merge_check.merge_possible = merge_status
1691 1691 merge_check.merge_msg = msg
1692 1692 if not merge_status:
1693 1693 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1694 1694 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1695 1695
1696 1696 if fail_early:
1697 1697 return merge_check
1698 1698
1699 1699 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1700 1700 return merge_check
1701 1701
1702 1702 @classmethod
1703 1703 def get_merge_conditions(cls, pull_request, translator):
1704 1704 _ = translator
1705 1705 merge_details = {}
1706 1706
1707 1707 model = PullRequestModel()
1708 1708 use_rebase = model._use_rebase_for_merging(pull_request)
1709 1709
1710 1710 if use_rebase:
1711 1711 merge_details['merge_strategy'] = dict(
1712 1712 details={},
1713 1713 message=_('Merge strategy: rebase')
1714 1714 )
1715 1715 else:
1716 1716 merge_details['merge_strategy'] = dict(
1717 1717 details={},
1718 1718 message=_('Merge strategy: explicit merge commit')
1719 1719 )
1720 1720
1721 1721 close_branch = model._close_branch_before_merging(pull_request)
1722 1722 if close_branch:
1723 1723 repo_type = pull_request.target_repo.repo_type
1724 1724 close_msg = ''
1725 1725 if repo_type == 'hg':
1726 1726 close_msg = _('Source branch will be closed after merge.')
1727 1727 elif repo_type == 'git':
1728 1728 close_msg = _('Source branch will be deleted after merge.')
1729 1729
1730 1730 merge_details['close_branch'] = dict(
1731 1731 details={},
1732 1732 message=close_msg
1733 1733 )
1734 1734
1735 1735 return merge_details
1736 1736
1737 1737
1738 1738 ChangeTuple = collections.namedtuple(
1739 1739 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1740 1740
1741 1741 FileChangeTuple = collections.namedtuple(
1742 1742 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,2796 +1,2804 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-size: 120%;
38 38 color: white;
39 39 background-color: @alert2;
40 40 padding: 5px 0 5px 0;
41 41 font-weight: @text-semibold-weight;
42 42 font-family: @text-semibold;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .action-link.disabled {
104 104 color: @grey4;
105 105 cursor: inherit;
106 106 }
107 107
108 108 .clipboard-action {
109 109 cursor: pointer;
110 110 color: @grey4;
111 111 margin-left: 5px;
112 112
113 113 &:hover {
114 114 color: @grey2;
115 115 }
116 116 }
117 117
118 118 ul.simple-list{
119 119 list-style: none;
120 120 margin: 0;
121 121 padding: 0;
122 122 }
123 123
124 124 .main-content {
125 125 padding-bottom: @pagepadding;
126 126 }
127 127
128 128 .wide-mode-wrapper {
129 129 max-width:4000px !important;
130 130 }
131 131
132 132 .wrapper {
133 133 position: relative;
134 134 max-width: @wrapper-maxwidth;
135 135 margin: 0 auto;
136 136 }
137 137
138 138 #content {
139 139 clear: both;
140 140 padding: 0 @contentpadding;
141 141 }
142 142
143 143 .advanced-settings-fields{
144 144 input{
145 145 margin-left: @textmargin;
146 146 margin-right: @padding/2;
147 147 }
148 148 }
149 149
150 150 .cs_files_title {
151 151 margin: @pagepadding 0 0;
152 152 }
153 153
154 154 input.inline[type="file"] {
155 155 display: inline;
156 156 }
157 157
158 158 .error_page {
159 159 margin: 10% auto;
160 160
161 161 h1 {
162 162 color: @grey2;
163 163 }
164 164
165 165 .alert {
166 166 margin: @padding 0;
167 167 }
168 168
169 169 .error-branding {
170 170 color: @grey4;
171 171 font-weight: @text-semibold-weight;
172 172 font-family: @text-semibold;
173 173 }
174 174
175 175 .error_message {
176 176 font-family: @text-regular;
177 177 }
178 178
179 179 .sidebar {
180 180 min-height: 275px;
181 181 margin: 0;
182 182 padding: 0 0 @sidebarpadding @sidebarpadding;
183 183 border: none;
184 184 }
185 185
186 186 .main-content {
187 187 position: relative;
188 188 margin: 0 @sidebarpadding @sidebarpadding;
189 189 padding: 0 0 0 @sidebarpadding;
190 190 border-left: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 margin: 0;
196 196 border: none;
197 197 }
198 198 }
199 199
200 200 .inner-column {
201 201 float: left;
202 202 width: 29.75%;
203 203 min-height: 150px;
204 204 margin: @sidebarpadding 2% 0 0;
205 205 padding: 0 2% 0 0;
206 206 border-right: @border-thickness solid @grey5;
207 207
208 208 @media (max-width:767px) {
209 209 clear: both;
210 210 width: 100%;
211 211 border: none;
212 212 }
213 213
214 214 ul {
215 215 padding-left: 1.25em;
216 216 }
217 217
218 218 &:last-child {
219 219 margin: @sidebarpadding 0 0;
220 220 border: none;
221 221 }
222 222
223 223 h4 {
224 224 margin: 0 0 @padding;
225 225 font-weight: @text-semibold-weight;
226 226 font-family: @text-semibold;
227 227 }
228 228 }
229 229 }
230 230 .error-page-logo {
231 231 width: 130px;
232 232 height: 160px;
233 233 }
234 234
235 235 // HEADER
236 236 .header {
237 237
238 238 // TODO: johbo: Fix login pages, so that they work without a min-height
239 239 // for the header and then remove the min-height. I chose a smaller value
240 240 // intentionally here to avoid rendering issues in the main navigation.
241 241 min-height: 49px;
242 242
243 243 position: relative;
244 244 vertical-align: bottom;
245 245 padding: 0 @header-padding;
246 246 background-color: @grey1;
247 247 color: @grey5;
248 248
249 249 .title {
250 250 overflow: visible;
251 251 }
252 252
253 253 &:before,
254 254 &:after {
255 255 content: "";
256 256 clear: both;
257 257 width: 100%;
258 258 }
259 259
260 260 // TODO: johbo: Avoids breaking "Repositories" chooser
261 261 .select2-container .select2-choice .select2-arrow {
262 262 display: none;
263 263 }
264 264 }
265 265
266 266 #header-inner {
267 267 &.title {
268 268 margin: 0;
269 269 }
270 270 &:before,
271 271 &:after {
272 272 content: "";
273 273 clear: both;
274 274 }
275 275 }
276 276
277 277 // Gists
278 278 #files_data {
279 279 clear: both; //for firefox
280 280 padding-top: 10px;
281 281 }
282 282
283 283 #gistid {
284 284 margin-right: @padding;
285 285 }
286 286
287 287 // Global Settings Editor
288 288 .textarea.editor {
289 289 float: left;
290 290 position: relative;
291 291 max-width: @texteditor-width;
292 292
293 293 select {
294 294 position: absolute;
295 295 top:10px;
296 296 right:0;
297 297 }
298 298
299 299 .CodeMirror {
300 300 margin: 0;
301 301 }
302 302
303 303 .help-block {
304 304 margin: 0 0 @padding;
305 305 padding:.5em;
306 306 background-color: @grey6;
307 307 &.pre-formatting {
308 308 white-space: pre;
309 309 }
310 310 }
311 311 }
312 312
313 313 ul.auth_plugins {
314 314 margin: @padding 0 @padding @legend-width;
315 315 padding: 0;
316 316
317 317 li {
318 318 margin-bottom: @padding;
319 319 line-height: 1em;
320 320 list-style-type: none;
321 321
322 322 .auth_buttons .btn {
323 323 margin-right: @padding;
324 324 }
325 325
326 326 }
327 327 }
328 328
329 329
330 330 // My Account PR list
331 331
332 332 #show_closed {
333 333 margin: 0 1em 0 0;
334 334 }
335 335
336 .pullrequestlist {
336 #pull_request_list_table {
337 337 .closed {
338 338 background-color: @grey6;
339 339 }
340
341 .state-creating,
342 .state-updating,
343 .state-merging
344 {
345 background-color: @grey6;
346 }
347
340 348 .td-status {
341 349 padding-left: .5em;
342 350 }
343 351 .log-container .truncate {
344 352 height: 2.75em;
345 353 white-space: pre-line;
346 354 }
347 355 table.rctable .user {
348 356 padding-left: 0;
349 357 }
350 358 table.rctable {
351 359 td.td-description,
352 360 .rc-user {
353 361 min-width: auto;
354 362 }
355 363 }
356 364 }
357 365
358 366 // Pull Requests
359 367
360 368 .pullrequests_section_head {
361 369 display: block;
362 370 clear: both;
363 371 margin: @padding 0;
364 372 font-weight: @text-bold-weight;
365 373 font-family: @text-bold;
366 374 }
367 375
368 376 .pr-origininfo, .pr-targetinfo {
369 377 position: relative;
370 378
371 379 .tag {
372 380 display: inline-block;
373 381 margin: 0 1em .5em 0;
374 382 }
375 383
376 384 .clone-url {
377 385 display: inline-block;
378 386 margin: 0 0 .5em 0;
379 387 padding: 0;
380 388 line-height: 1.2em;
381 389 }
382 390 }
383 391
384 392 .pr-mergeinfo {
385 393 min-width: 95% !important;
386 394 padding: 0 !important;
387 395 border: 0;
388 396 }
389 397 .pr-mergeinfo-copy {
390 398 padding: 0 0;
391 399 }
392 400
393 401 .pr-pullinfo {
394 402 min-width: 95% !important;
395 403 padding: 0 !important;
396 404 border: 0;
397 405 }
398 406 .pr-pullinfo-copy {
399 407 padding: 0 0;
400 408 }
401 409
402 410
403 411 #pr-title-input {
404 412 width: 72%;
405 413 font-size: 1em;
406 414 margin: 0;
407 415 padding: 0 0 0 @padding/4;
408 416 line-height: 1.7em;
409 417 color: @text-color;
410 418 letter-spacing: .02em;
411 419 font-weight: @text-bold-weight;
412 420 font-family: @text-bold;
413 421 }
414 422
415 423 #pullrequest_title {
416 424 width: 100%;
417 425 box-sizing: border-box;
418 426 }
419 427
420 428 #pr_open_message {
421 429 border: @border-thickness solid #fff;
422 430 border-radius: @border-radius;
423 431 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
424 432 text-align: left;
425 433 overflow: hidden;
426 434 }
427 435
428 436 .pr-submit-button {
429 437 float: right;
430 438 margin: 0 0 0 5px;
431 439 }
432 440
433 441 .pr-spacing-container {
434 442 padding: 20px;
435 443 clear: both
436 444 }
437 445
438 446 #pr-description-input {
439 447 margin-bottom: 0;
440 448 }
441 449
442 450 .pr-description-label {
443 451 vertical-align: top;
444 452 }
445 453
446 454 .perms_section_head {
447 455 min-width: 625px;
448 456
449 457 h2 {
450 458 margin-bottom: 0;
451 459 }
452 460
453 461 .label-checkbox {
454 462 float: left;
455 463 }
456 464
457 465 &.field {
458 466 margin: @space 0 @padding;
459 467 }
460 468
461 469 &:first-child.field {
462 470 margin-top: 0;
463 471
464 472 .label {
465 473 margin-top: 0;
466 474 padding-top: 0;
467 475 }
468 476
469 477 .radios {
470 478 padding-top: 0;
471 479 }
472 480 }
473 481
474 482 .radios {
475 483 position: relative;
476 484 width: 505px;
477 485 }
478 486 }
479 487
480 488 //--- MODULES ------------------//
481 489
482 490
483 491 // Server Announcement
484 492 #server-announcement {
485 493 width: 95%;
486 494 margin: @padding auto;
487 495 padding: @padding;
488 496 border-width: 2px;
489 497 border-style: solid;
490 498 .border-radius(2px);
491 499 font-weight: @text-bold-weight;
492 500 font-family: @text-bold;
493 501
494 502 &.info { border-color: @alert4; background-color: @alert4-inner; }
495 503 &.warning { border-color: @alert3; background-color: @alert3-inner; }
496 504 &.error { border-color: @alert2; background-color: @alert2-inner; }
497 505 &.success { border-color: @alert1; background-color: @alert1-inner; }
498 506 &.neutral { border-color: @grey3; background-color: @grey6; }
499 507 }
500 508
501 509 // Fixed Sidebar Column
502 510 .sidebar-col-wrapper {
503 511 padding-left: @sidebar-all-width;
504 512
505 513 .sidebar {
506 514 width: @sidebar-width;
507 515 margin-left: -@sidebar-all-width;
508 516 }
509 517 }
510 518
511 519 .sidebar-col-wrapper.scw-small {
512 520 padding-left: @sidebar-small-all-width;
513 521
514 522 .sidebar {
515 523 width: @sidebar-small-width;
516 524 margin-left: -@sidebar-small-all-width;
517 525 }
518 526 }
519 527
520 528
521 529 // FOOTER
522 530 #footer {
523 531 padding: 0;
524 532 text-align: center;
525 533 vertical-align: middle;
526 534 color: @grey2;
527 535 font-size: 11px;
528 536
529 537 p {
530 538 margin: 0;
531 539 padding: 1em;
532 540 line-height: 1em;
533 541 }
534 542
535 543 .server-instance { //server instance
536 544 display: none;
537 545 }
538 546
539 547 .title {
540 548 float: none;
541 549 margin: 0 auto;
542 550 }
543 551 }
544 552
545 553 button.close {
546 554 padding: 0;
547 555 cursor: pointer;
548 556 background: transparent;
549 557 border: 0;
550 558 .box-shadow(none);
551 559 -webkit-appearance: none;
552 560 }
553 561
554 562 .close {
555 563 float: right;
556 564 font-size: 21px;
557 565 font-family: @text-bootstrap;
558 566 line-height: 1em;
559 567 font-weight: bold;
560 568 color: @grey2;
561 569
562 570 &:hover,
563 571 &:focus {
564 572 color: @grey1;
565 573 text-decoration: none;
566 574 cursor: pointer;
567 575 }
568 576 }
569 577
570 578 // GRID
571 579 .sorting,
572 580 .sorting_desc,
573 581 .sorting_asc {
574 582 cursor: pointer;
575 583 }
576 584 .sorting_desc:after {
577 585 content: "\00A0\25B2";
578 586 font-size: .75em;
579 587 }
580 588 .sorting_asc:after {
581 589 content: "\00A0\25BC";
582 590 font-size: .68em;
583 591 }
584 592
585 593
586 594 .user_auth_tokens {
587 595
588 596 &.truncate {
589 597 white-space: nowrap;
590 598 overflow: hidden;
591 599 text-overflow: ellipsis;
592 600 }
593 601
594 602 .fields .field .input {
595 603 margin: 0;
596 604 }
597 605
598 606 input#description {
599 607 width: 100px;
600 608 margin: 0;
601 609 }
602 610
603 611 .drop-menu {
604 612 // TODO: johbo: Remove this, should work out of the box when
605 613 // having multiple inputs inline
606 614 margin: 0 0 0 5px;
607 615 }
608 616 }
609 617 #user_list_table {
610 618 .closed {
611 619 background-color: @grey6;
612 620 }
613 621 }
614 622
615 623
616 624 input, textarea {
617 625 &.disabled {
618 626 opacity: .5;
619 627 }
620 628
621 629 &:hover {
622 630 border-color: @grey3;
623 631 box-shadow: @button-shadow;
624 632 }
625 633
626 634 &:focus {
627 635 border-color: @rcblue;
628 636 box-shadow: @button-shadow;
629 637 }
630 638 }
631 639
632 640 // remove extra padding in firefox
633 641 input::-moz-focus-inner { border:0; padding:0 }
634 642
635 643 .adjacent input {
636 644 margin-bottom: @padding;
637 645 }
638 646
639 647 .permissions_boxes {
640 648 display: block;
641 649 }
642 650
643 651 //FORMS
644 652
645 653 .medium-inline,
646 654 input#description.medium-inline {
647 655 display: inline;
648 656 width: @medium-inline-input-width;
649 657 min-width: 100px;
650 658 }
651 659
652 660 select {
653 661 //reset
654 662 -webkit-appearance: none;
655 663 -moz-appearance: none;
656 664
657 665 display: inline-block;
658 666 height: 28px;
659 667 width: auto;
660 668 margin: 0 @padding @padding 0;
661 669 padding: 0 18px 0 8px;
662 670 line-height:1em;
663 671 font-size: @basefontsize;
664 672 border: @border-thickness solid @grey5;
665 673 border-radius: @border-radius;
666 674 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
667 675 color: @grey4;
668 676 box-shadow: @button-shadow;
669 677
670 678 &:after {
671 679 content: "\00A0\25BE";
672 680 }
673 681
674 682 &:focus, &:hover {
675 683 outline: none;
676 684 border-color: @grey4;
677 685 color: @rcdarkblue;
678 686 }
679 687 }
680 688
681 689 option {
682 690 &:focus {
683 691 outline: none;
684 692 }
685 693 }
686 694
687 695 input,
688 696 textarea {
689 697 padding: @input-padding;
690 698 border: @input-border-thickness solid @border-highlight-color;
691 699 .border-radius (@border-radius);
692 700 font-family: @text-light;
693 701 font-size: @basefontsize;
694 702
695 703 &.input-sm {
696 704 padding: 5px;
697 705 }
698 706
699 707 &#description {
700 708 min-width: @input-description-minwidth;
701 709 min-height: 1em;
702 710 padding: 10px;
703 711 }
704 712 }
705 713
706 714 .field-sm {
707 715 input,
708 716 textarea {
709 717 padding: 5px;
710 718 }
711 719 }
712 720
713 721 textarea {
714 722 display: block;
715 723 clear: both;
716 724 width: 100%;
717 725 min-height: 100px;
718 726 margin-bottom: @padding;
719 727 .box-sizing(border-box);
720 728 overflow: auto;
721 729 }
722 730
723 731 label {
724 732 font-family: @text-light;
725 733 }
726 734
727 735 // GRAVATARS
728 736 // centers gravatar on username to the right
729 737
730 738 .gravatar {
731 739 display: inline;
732 740 min-width: 16px;
733 741 min-height: 16px;
734 742 margin: -5px 0;
735 743 padding: 0;
736 744 line-height: 1em;
737 745 box-sizing: content-box;
738 746 border-radius: 50%;
739 747
740 748 &.gravatar-large {
741 749 margin: -0.5em .25em -0.5em 0;
742 750 }
743 751
744 752 & + .user {
745 753 display: inline;
746 754 margin: 0;
747 755 padding: 0 0 0 .17em;
748 756 line-height: 1em;
749 757 }
750 758 }
751 759
752 760 .user-inline-data {
753 761 display: inline-block;
754 762 float: left;
755 763 padding-left: .5em;
756 764 line-height: 1.3em;
757 765 }
758 766
759 767 .rc-user { // gravatar + user wrapper
760 768 float: left;
761 769 position: relative;
762 770 min-width: 100px;
763 771 max-width: 200px;
764 772 min-height: (@gravatar-size + @border-thickness * 2); // account for border
765 773 display: block;
766 774 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
767 775
768 776
769 777 .gravatar {
770 778 display: block;
771 779 position: absolute;
772 780 top: 0;
773 781 left: 0;
774 782 min-width: @gravatar-size;
775 783 min-height: @gravatar-size;
776 784 margin: 0;
777 785 }
778 786
779 787 .user {
780 788 display: block;
781 789 max-width: 175px;
782 790 padding-top: 2px;
783 791 overflow: hidden;
784 792 text-overflow: ellipsis;
785 793 }
786 794 }
787 795
788 796 .gist-gravatar,
789 797 .journal_container {
790 798 .gravatar-large {
791 799 margin: 0 .5em -10px 0;
792 800 }
793 801 }
794 802
795 803
796 804 // ADMIN SETTINGS
797 805
798 806 // Tag Patterns
799 807 .tag_patterns {
800 808 .tag_input {
801 809 margin-bottom: @padding;
802 810 }
803 811 }
804 812
805 813 .locked_input {
806 814 position: relative;
807 815
808 816 input {
809 817 display: inline;
810 818 margin: 3px 5px 0px 0px;
811 819 }
812 820
813 821 br {
814 822 display: none;
815 823 }
816 824
817 825 .error-message {
818 826 float: left;
819 827 width: 100%;
820 828 }
821 829
822 830 .lock_input_button {
823 831 display: inline;
824 832 }
825 833
826 834 .help-block {
827 835 clear: both;
828 836 }
829 837 }
830 838
831 839 // Notifications
832 840
833 841 .notifications_buttons {
834 842 margin: 0 0 @space 0;
835 843 padding: 0;
836 844
837 845 .btn {
838 846 display: inline-block;
839 847 }
840 848 }
841 849
842 850 .notification-list {
843 851
844 852 div {
845 853 display: inline-block;
846 854 vertical-align: middle;
847 855 }
848 856
849 857 .container {
850 858 display: block;
851 859 margin: 0 0 @padding 0;
852 860 }
853 861
854 862 .delete-notifications {
855 863 margin-left: @padding;
856 864 text-align: right;
857 865 cursor: pointer;
858 866 }
859 867
860 868 .read-notifications {
861 869 margin-left: @padding/2;
862 870 text-align: right;
863 871 width: 35px;
864 872 cursor: pointer;
865 873 }
866 874
867 875 .icon-minus-sign {
868 876 color: @alert2;
869 877 }
870 878
871 879 .icon-ok-sign {
872 880 color: @alert1;
873 881 }
874 882 }
875 883
876 884 .user_settings {
877 885 float: left;
878 886 clear: both;
879 887 display: block;
880 888 width: 100%;
881 889
882 890 .gravatar_box {
883 891 margin-bottom: @padding;
884 892
885 893 &:after {
886 894 content: " ";
887 895 clear: both;
888 896 width: 100%;
889 897 }
890 898 }
891 899
892 900 .fields .field {
893 901 clear: both;
894 902 }
895 903 }
896 904
897 905 .advanced_settings {
898 906 margin-bottom: @space;
899 907
900 908 .help-block {
901 909 margin-left: 0;
902 910 }
903 911
904 912 button + .help-block {
905 913 margin-top: @padding;
906 914 }
907 915 }
908 916
909 917 // admin settings radio buttons and labels
910 918 .label-2 {
911 919 float: left;
912 920 width: @label2-width;
913 921
914 922 label {
915 923 color: @grey1;
916 924 }
917 925 }
918 926 .checkboxes {
919 927 float: left;
920 928 width: @checkboxes-width;
921 929 margin-bottom: @padding;
922 930
923 931 .checkbox {
924 932 width: 100%;
925 933
926 934 label {
927 935 margin: 0;
928 936 padding: 0;
929 937 }
930 938 }
931 939
932 940 .checkbox + .checkbox {
933 941 display: inline-block;
934 942 }
935 943
936 944 label {
937 945 margin-right: 1em;
938 946 }
939 947 }
940 948
941 949 // CHANGELOG
942 950 .container_header {
943 951 float: left;
944 952 display: block;
945 953 width: 100%;
946 954 margin: @padding 0 @padding;
947 955
948 956 #filter_changelog {
949 957 float: left;
950 958 margin-right: @padding;
951 959 }
952 960
953 961 .breadcrumbs_light {
954 962 display: inline-block;
955 963 }
956 964 }
957 965
958 966 .info_box {
959 967 float: right;
960 968 }
961 969
962 970
963 971
964 972 #graph_content{
965 973
966 974 // adjust for table headers so that graph renders properly
967 975 // #graph_nodes padding - table cell padding
968 976 padding-top: (@space - (@basefontsize * 2.4));
969 977
970 978 &.graph_full_width {
971 979 width: 100%;
972 980 max-width: 100%;
973 981 }
974 982 }
975 983
976 984 #graph {
977 985 .flag_status {
978 986 margin: 0;
979 987 }
980 988
981 989 .pagination-left {
982 990 float: left;
983 991 clear: both;
984 992 }
985 993
986 994 .log-container {
987 995 max-width: 345px;
988 996
989 997 .message{
990 998 max-width: 340px;
991 999 }
992 1000 }
993 1001
994 1002 .graph-col-wrapper {
995 1003
996 1004 #graph_nodes {
997 1005 width: 100px;
998 1006 position: absolute;
999 1007 left: 70px;
1000 1008 z-index: -1;
1001 1009 }
1002 1010 }
1003 1011
1004 1012 .load-more-commits {
1005 1013 text-align: center;
1006 1014 }
1007 1015 .load-more-commits:hover {
1008 1016 background-color: @grey7;
1009 1017 }
1010 1018 .load-more-commits {
1011 1019 a {
1012 1020 display: block;
1013 1021 }
1014 1022 }
1015 1023 }
1016 1024
1017 1025 .obsolete-toggle {
1018 1026 line-height: 30px;
1019 1027 margin-left: -15px;
1020 1028 }
1021 1029
1022 1030 #rev_range_container, #rev_range_clear, #rev_range_more {
1023 1031 margin-top: -5px;
1024 1032 margin-bottom: -5px;
1025 1033 }
1026 1034
1027 1035 #filter_changelog {
1028 1036 float: left;
1029 1037 }
1030 1038
1031 1039
1032 1040 //--- THEME ------------------//
1033 1041
1034 1042 #logo {
1035 1043 float: left;
1036 1044 margin: 9px 0 0 0;
1037 1045
1038 1046 .header {
1039 1047 background-color: transparent;
1040 1048 }
1041 1049
1042 1050 a {
1043 1051 display: inline-block;
1044 1052 }
1045 1053
1046 1054 img {
1047 1055 height:30px;
1048 1056 }
1049 1057 }
1050 1058
1051 1059 .logo-wrapper {
1052 1060 float:left;
1053 1061 }
1054 1062
1055 1063 .branding {
1056 1064 float: left;
1057 1065 padding: 9px 2px;
1058 1066 line-height: 1em;
1059 1067 font-size: @navigation-fontsize;
1060 1068
1061 1069 a {
1062 1070 color: @grey5
1063 1071 }
1064 1072 }
1065 1073
1066 1074 img {
1067 1075 border: none;
1068 1076 outline: none;
1069 1077 }
1070 1078 user-profile-header
1071 1079 label {
1072 1080
1073 1081 input[type="checkbox"] {
1074 1082 margin-right: 1em;
1075 1083 }
1076 1084 input[type="radio"] {
1077 1085 margin-right: 1em;
1078 1086 }
1079 1087 }
1080 1088
1081 1089 .flag_status {
1082 1090 margin: 2px;
1083 1091 &.under_review {
1084 1092 .circle(5px, @alert3);
1085 1093 }
1086 1094 &.approved {
1087 1095 .circle(5px, @alert1);
1088 1096 }
1089 1097 &.rejected,
1090 1098 &.forced_closed{
1091 1099 .circle(5px, @alert2);
1092 1100 }
1093 1101 &.not_reviewed {
1094 1102 .circle(5px, @grey5);
1095 1103 }
1096 1104 }
1097 1105
1098 1106 .flag_status_comment_box {
1099 1107 margin: 5px 6px 0px 2px;
1100 1108 }
1101 1109 .test_pattern_preview {
1102 1110 margin: @space 0;
1103 1111
1104 1112 p {
1105 1113 margin-bottom: 0;
1106 1114 border-bottom: @border-thickness solid @border-default-color;
1107 1115 color: @grey3;
1108 1116 }
1109 1117
1110 1118 .btn {
1111 1119 margin-bottom: @padding;
1112 1120 }
1113 1121 }
1114 1122 #test_pattern_result {
1115 1123 display: none;
1116 1124 &:extend(pre);
1117 1125 padding: .9em;
1118 1126 color: @grey3;
1119 1127 background-color: @grey7;
1120 1128 border-right: @border-thickness solid @border-default-color;
1121 1129 border-bottom: @border-thickness solid @border-default-color;
1122 1130 border-left: @border-thickness solid @border-default-color;
1123 1131 }
1124 1132
1125 1133 #repo_vcs_settings {
1126 1134 #inherit_overlay_vcs_default {
1127 1135 display: none;
1128 1136 }
1129 1137 #inherit_overlay_vcs_custom {
1130 1138 display: custom;
1131 1139 }
1132 1140 &.inherited {
1133 1141 #inherit_overlay_vcs_default {
1134 1142 display: block;
1135 1143 }
1136 1144 #inherit_overlay_vcs_custom {
1137 1145 display: none;
1138 1146 }
1139 1147 }
1140 1148 }
1141 1149
1142 1150 .issue-tracker-link {
1143 1151 color: @rcblue;
1144 1152 }
1145 1153
1146 1154 // Issue Tracker Table Show/Hide
1147 1155 #repo_issue_tracker {
1148 1156 #inherit_overlay {
1149 1157 display: none;
1150 1158 }
1151 1159 #custom_overlay {
1152 1160 display: custom;
1153 1161 }
1154 1162 &.inherited {
1155 1163 #inherit_overlay {
1156 1164 display: block;
1157 1165 }
1158 1166 #custom_overlay {
1159 1167 display: none;
1160 1168 }
1161 1169 }
1162 1170 }
1163 1171 table.issuetracker {
1164 1172 &.readonly {
1165 1173 tr, td {
1166 1174 color: @grey3;
1167 1175 }
1168 1176 }
1169 1177 .edit {
1170 1178 display: none;
1171 1179 }
1172 1180 .editopen {
1173 1181 .edit {
1174 1182 display: inline;
1175 1183 }
1176 1184 .entry {
1177 1185 display: none;
1178 1186 }
1179 1187 }
1180 1188 tr td.td-action {
1181 1189 min-width: 117px;
1182 1190 }
1183 1191 td input {
1184 1192 max-width: none;
1185 1193 min-width: 30px;
1186 1194 width: 80%;
1187 1195 }
1188 1196 .issuetracker_pref input {
1189 1197 width: 40%;
1190 1198 }
1191 1199 input.edit_issuetracker_update {
1192 1200 margin-right: 0;
1193 1201 width: auto;
1194 1202 }
1195 1203 }
1196 1204
1197 1205 table.integrations {
1198 1206 .td-icon {
1199 1207 width: 20px;
1200 1208 .integration-icon {
1201 1209 height: 20px;
1202 1210 width: 20px;
1203 1211 }
1204 1212 }
1205 1213 }
1206 1214
1207 1215 .integrations {
1208 1216 a.integration-box {
1209 1217 color: @text-color;
1210 1218 &:hover {
1211 1219 .panel {
1212 1220 background: #fbfbfb;
1213 1221 }
1214 1222 }
1215 1223 .integration-icon {
1216 1224 width: 30px;
1217 1225 height: 30px;
1218 1226 margin-right: 20px;
1219 1227 float: left;
1220 1228 }
1221 1229
1222 1230 .panel-body {
1223 1231 padding: 10px;
1224 1232 }
1225 1233 .panel {
1226 1234 margin-bottom: 10px;
1227 1235 }
1228 1236 h2 {
1229 1237 display: inline-block;
1230 1238 margin: 0;
1231 1239 min-width: 140px;
1232 1240 }
1233 1241 }
1234 1242 a.integration-box.dummy-integration {
1235 1243 color: @grey4
1236 1244 }
1237 1245 }
1238 1246
1239 1247 //Permissions Settings
1240 1248 #add_perm {
1241 1249 margin: 0 0 @padding;
1242 1250 cursor: pointer;
1243 1251 }
1244 1252
1245 1253 .perm_ac {
1246 1254 input {
1247 1255 width: 95%;
1248 1256 }
1249 1257 }
1250 1258
1251 1259 .autocomplete-suggestions {
1252 1260 width: auto !important; // overrides autocomplete.js
1253 1261 min-width: 278px;
1254 1262 margin: 0;
1255 1263 border: @border-thickness solid @grey5;
1256 1264 border-radius: @border-radius;
1257 1265 color: @grey2;
1258 1266 background-color: white;
1259 1267 }
1260 1268
1261 1269 .autocomplete-qfilter-suggestions {
1262 1270 width: auto !important; // overrides autocomplete.js
1263 1271 max-height: 100% !important;
1264 1272 min-width: 376px;
1265 1273 margin: 0;
1266 1274 border: @border-thickness solid @grey5;
1267 1275 color: @grey2;
1268 1276 background-color: white;
1269 1277 }
1270 1278
1271 1279 .autocomplete-selected {
1272 1280 background: #F0F0F0;
1273 1281 }
1274 1282
1275 1283 .ac-container-wrap {
1276 1284 margin: 0;
1277 1285 padding: 8px;
1278 1286 border-bottom: @border-thickness solid @grey5;
1279 1287 list-style-type: none;
1280 1288 cursor: pointer;
1281 1289
1282 1290 &:hover {
1283 1291 background-color: @grey7;
1284 1292 }
1285 1293
1286 1294 img {
1287 1295 height: @gravatar-size;
1288 1296 width: @gravatar-size;
1289 1297 margin-right: 1em;
1290 1298 }
1291 1299
1292 1300 strong {
1293 1301 font-weight: normal;
1294 1302 }
1295 1303 }
1296 1304
1297 1305 // Settings Dropdown
1298 1306 .user-menu .container {
1299 1307 padding: 0 4px;
1300 1308 margin: 0;
1301 1309 }
1302 1310
1303 1311 .user-menu .gravatar {
1304 1312 cursor: pointer;
1305 1313 }
1306 1314
1307 1315 .codeblock {
1308 1316 margin-bottom: @padding;
1309 1317 clear: both;
1310 1318
1311 1319 .stats {
1312 1320 overflow: hidden;
1313 1321 }
1314 1322
1315 1323 .message{
1316 1324 textarea{
1317 1325 margin: 0;
1318 1326 }
1319 1327 }
1320 1328
1321 1329 .code-header {
1322 1330 .stats {
1323 1331 line-height: 2em;
1324 1332
1325 1333 .revision_id {
1326 1334 margin-left: 0;
1327 1335 }
1328 1336 .buttons {
1329 1337 padding-right: 0;
1330 1338 }
1331 1339 }
1332 1340
1333 1341 .item{
1334 1342 margin-right: 0.5em;
1335 1343 }
1336 1344 }
1337 1345
1338 1346 #editor_container {
1339 1347 position: relative;
1340 1348 margin: @padding 10px;
1341 1349 }
1342 1350 }
1343 1351
1344 1352 #file_history_container {
1345 1353 display: none;
1346 1354 }
1347 1355
1348 1356 .file-history-inner {
1349 1357 margin-bottom: 10px;
1350 1358 }
1351 1359
1352 1360 // Pull Requests
1353 1361 .summary-details {
1354 1362 width: 72%;
1355 1363 }
1356 1364 .pr-summary {
1357 1365 border-bottom: @border-thickness solid @grey5;
1358 1366 margin-bottom: @space;
1359 1367 }
1360 1368 .reviewers-title {
1361 1369 width: 25%;
1362 1370 min-width: 200px;
1363 1371 }
1364 1372 .reviewers {
1365 1373 width: 25%;
1366 1374 min-width: 200px;
1367 1375 }
1368 1376 .reviewers ul li {
1369 1377 position: relative;
1370 1378 width: 100%;
1371 1379 padding-bottom: 8px;
1372 1380 list-style-type: none;
1373 1381 }
1374 1382
1375 1383 .reviewer_entry {
1376 1384 min-height: 55px;
1377 1385 }
1378 1386
1379 1387 .reviewers_member {
1380 1388 width: 100%;
1381 1389 overflow: auto;
1382 1390 }
1383 1391 .reviewer_reason {
1384 1392 padding-left: 20px;
1385 1393 line-height: 1.5em;
1386 1394 }
1387 1395 .reviewer_status {
1388 1396 display: inline-block;
1389 1397 vertical-align: top;
1390 1398 width: 25px;
1391 1399 min-width: 25px;
1392 1400 height: 1.2em;
1393 1401 margin-top: 3px;
1394 1402 line-height: 1em;
1395 1403 }
1396 1404
1397 1405 .reviewer_name {
1398 1406 display: inline-block;
1399 1407 max-width: 83%;
1400 1408 padding-right: 20px;
1401 1409 vertical-align: middle;
1402 1410 line-height: 1;
1403 1411
1404 1412 .rc-user {
1405 1413 min-width: 0;
1406 1414 margin: -2px 1em 0 0;
1407 1415 }
1408 1416
1409 1417 .reviewer {
1410 1418 float: left;
1411 1419 }
1412 1420 }
1413 1421
1414 1422 .reviewer_member_mandatory {
1415 1423 position: absolute;
1416 1424 left: 15px;
1417 1425 top: 8px;
1418 1426 width: 16px;
1419 1427 font-size: 11px;
1420 1428 margin: 0;
1421 1429 padding: 0;
1422 1430 color: black;
1423 1431 }
1424 1432
1425 1433 .reviewer_member_mandatory_remove,
1426 1434 .reviewer_member_remove {
1427 1435 position: absolute;
1428 1436 right: 0;
1429 1437 top: 0;
1430 1438 width: 16px;
1431 1439 margin-bottom: 10px;
1432 1440 padding: 0;
1433 1441 color: black;
1434 1442 }
1435 1443
1436 1444 .reviewer_member_mandatory_remove {
1437 1445 color: @grey4;
1438 1446 }
1439 1447
1440 1448 .reviewer_member_status {
1441 1449 margin-top: 5px;
1442 1450 }
1443 1451 .pr-summary #summary{
1444 1452 width: 100%;
1445 1453 }
1446 1454 .pr-summary .action_button:hover {
1447 1455 border: 0;
1448 1456 cursor: pointer;
1449 1457 }
1450 1458 .pr-details-title {
1451 1459 padding-bottom: 8px;
1452 1460 border-bottom: @border-thickness solid @grey5;
1453 1461
1454 1462 .action_button.disabled {
1455 1463 color: @grey4;
1456 1464 cursor: inherit;
1457 1465 }
1458 1466 .action_button {
1459 1467 color: @rcblue;
1460 1468 }
1461 1469 }
1462 1470 .pr-details-content {
1463 1471 margin-top: @textmargin;
1464 1472 margin-bottom: @textmargin;
1465 1473 }
1466 1474
1467 1475 .pr-reviewer-rules {
1468 1476 padding: 10px 0px 20px 0px;
1469 1477 }
1470 1478
1471 1479 .group_members {
1472 1480 margin-top: 0;
1473 1481 padding: 0;
1474 1482 list-style: outside none none;
1475 1483
1476 1484 img {
1477 1485 height: @gravatar-size;
1478 1486 width: @gravatar-size;
1479 1487 margin-right: .5em;
1480 1488 margin-left: 3px;
1481 1489 }
1482 1490
1483 1491 .to-delete {
1484 1492 .user {
1485 1493 text-decoration: line-through;
1486 1494 }
1487 1495 }
1488 1496 }
1489 1497
1490 1498 .compare_view_commits_title {
1491 1499 .disabled {
1492 1500 cursor: inherit;
1493 1501 &:hover{
1494 1502 background-color: inherit;
1495 1503 color: inherit;
1496 1504 }
1497 1505 }
1498 1506 }
1499 1507
1500 1508 .subtitle-compare {
1501 1509 margin: -15px 0px 0px 0px;
1502 1510 }
1503 1511
1504 1512 .comments-summary-td {
1505 1513 border-top: 1px dashed @grey5;
1506 1514 }
1507 1515
1508 1516 // new entry in group_members
1509 1517 .td-author-new-entry {
1510 1518 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1511 1519 }
1512 1520
1513 1521 .usergroup_member_remove {
1514 1522 width: 16px;
1515 1523 margin-bottom: 10px;
1516 1524 padding: 0;
1517 1525 color: black !important;
1518 1526 cursor: pointer;
1519 1527 }
1520 1528
1521 1529 .reviewer_ac .ac-input {
1522 1530 width: 92%;
1523 1531 margin-bottom: 1em;
1524 1532 }
1525 1533
1526 1534 .compare_view_commits tr{
1527 1535 height: 20px;
1528 1536 }
1529 1537 .compare_view_commits td {
1530 1538 vertical-align: top;
1531 1539 padding-top: 10px;
1532 1540 }
1533 1541 .compare_view_commits .author {
1534 1542 margin-left: 5px;
1535 1543 }
1536 1544
1537 1545 .compare_view_commits {
1538 1546 .color-a {
1539 1547 color: @alert1;
1540 1548 }
1541 1549
1542 1550 .color-c {
1543 1551 color: @color3;
1544 1552 }
1545 1553
1546 1554 .color-r {
1547 1555 color: @color5;
1548 1556 }
1549 1557
1550 1558 .color-a-bg {
1551 1559 background-color: @alert1;
1552 1560 }
1553 1561
1554 1562 .color-c-bg {
1555 1563 background-color: @alert3;
1556 1564 }
1557 1565
1558 1566 .color-r-bg {
1559 1567 background-color: @alert2;
1560 1568 }
1561 1569
1562 1570 .color-a-border {
1563 1571 border: 1px solid @alert1;
1564 1572 }
1565 1573
1566 1574 .color-c-border {
1567 1575 border: 1px solid @alert3;
1568 1576 }
1569 1577
1570 1578 .color-r-border {
1571 1579 border: 1px solid @alert2;
1572 1580 }
1573 1581
1574 1582 .commit-change-indicator {
1575 1583 width: 15px;
1576 1584 height: 15px;
1577 1585 position: relative;
1578 1586 left: 15px;
1579 1587 }
1580 1588
1581 1589 .commit-change-content {
1582 1590 text-align: center;
1583 1591 vertical-align: middle;
1584 1592 line-height: 15px;
1585 1593 }
1586 1594 }
1587 1595
1588 1596 .compare_view_filepath {
1589 1597 color: @grey1;
1590 1598 }
1591 1599
1592 1600 .show_more {
1593 1601 display: inline-block;
1594 1602 width: 0;
1595 1603 height: 0;
1596 1604 vertical-align: middle;
1597 1605 content: "";
1598 1606 border: 4px solid;
1599 1607 border-right-color: transparent;
1600 1608 border-bottom-color: transparent;
1601 1609 border-left-color: transparent;
1602 1610 font-size: 0;
1603 1611 }
1604 1612
1605 1613 .journal_more .show_more {
1606 1614 display: inline;
1607 1615
1608 1616 &:after {
1609 1617 content: none;
1610 1618 }
1611 1619 }
1612 1620
1613 1621 .compare_view_commits .collapse_commit:after {
1614 1622 cursor: pointer;
1615 1623 content: "\00A0\25B4";
1616 1624 margin-left: -3px;
1617 1625 font-size: 17px;
1618 1626 color: @grey4;
1619 1627 }
1620 1628
1621 1629 .diff_links {
1622 1630 margin-left: 8px;
1623 1631 }
1624 1632
1625 1633 #pull_request_overview {
1626 1634 div.ancestor {
1627 1635 margin: -33px 0;
1628 1636 }
1629 1637 }
1630 1638
1631 1639 div.ancestor {
1632 1640 line-height: 33px;
1633 1641 }
1634 1642
1635 1643 .cs_icon_td input[type="checkbox"] {
1636 1644 display: none;
1637 1645 }
1638 1646
1639 1647 .cs_icon_td .expand_file_icon:after {
1640 1648 cursor: pointer;
1641 1649 content: "\00A0\25B6";
1642 1650 font-size: 12px;
1643 1651 color: @grey4;
1644 1652 }
1645 1653
1646 1654 .cs_icon_td .collapse_file_icon:after {
1647 1655 cursor: pointer;
1648 1656 content: "\00A0\25BC";
1649 1657 font-size: 12px;
1650 1658 color: @grey4;
1651 1659 }
1652 1660
1653 1661 /*new binary
1654 1662 NEW_FILENODE = 1
1655 1663 DEL_FILENODE = 2
1656 1664 MOD_FILENODE = 3
1657 1665 RENAMED_FILENODE = 4
1658 1666 COPIED_FILENODE = 5
1659 1667 CHMOD_FILENODE = 6
1660 1668 BIN_FILENODE = 7
1661 1669 */
1662 1670 .cs_files_expand {
1663 1671 font-size: @basefontsize + 5px;
1664 1672 line-height: 1.8em;
1665 1673 float: right;
1666 1674 }
1667 1675
1668 1676 .cs_files_expand span{
1669 1677 color: @rcblue;
1670 1678 cursor: pointer;
1671 1679 }
1672 1680 .cs_files {
1673 1681 clear: both;
1674 1682 padding-bottom: @padding;
1675 1683
1676 1684 .cur_cs {
1677 1685 margin: 10px 2px;
1678 1686 font-weight: bold;
1679 1687 }
1680 1688
1681 1689 .node {
1682 1690 float: left;
1683 1691 }
1684 1692
1685 1693 .changes {
1686 1694 float: right;
1687 1695 color: white;
1688 1696 font-size: @basefontsize - 4px;
1689 1697 margin-top: 4px;
1690 1698 opacity: 0.6;
1691 1699 filter: Alpha(opacity=60); /* IE8 and earlier */
1692 1700
1693 1701 .added {
1694 1702 background-color: @alert1;
1695 1703 float: left;
1696 1704 text-align: center;
1697 1705 }
1698 1706
1699 1707 .deleted {
1700 1708 background-color: @alert2;
1701 1709 float: left;
1702 1710 text-align: center;
1703 1711 }
1704 1712
1705 1713 .bin {
1706 1714 background-color: @alert1;
1707 1715 text-align: center;
1708 1716 }
1709 1717
1710 1718 /*new binary*/
1711 1719 .bin.bin1 {
1712 1720 background-color: @alert1;
1713 1721 text-align: center;
1714 1722 }
1715 1723
1716 1724 /*deleted binary*/
1717 1725 .bin.bin2 {
1718 1726 background-color: @alert2;
1719 1727 text-align: center;
1720 1728 }
1721 1729
1722 1730 /*mod binary*/
1723 1731 .bin.bin3 {
1724 1732 background-color: @grey2;
1725 1733 text-align: center;
1726 1734 }
1727 1735
1728 1736 /*rename file*/
1729 1737 .bin.bin4 {
1730 1738 background-color: @alert4;
1731 1739 text-align: center;
1732 1740 }
1733 1741
1734 1742 /*copied file*/
1735 1743 .bin.bin5 {
1736 1744 background-color: @alert4;
1737 1745 text-align: center;
1738 1746 }
1739 1747
1740 1748 /*chmod file*/
1741 1749 .bin.bin6 {
1742 1750 background-color: @grey2;
1743 1751 text-align: center;
1744 1752 }
1745 1753 }
1746 1754 }
1747 1755
1748 1756 .cs_files .cs_added, .cs_files .cs_A,
1749 1757 .cs_files .cs_added, .cs_files .cs_M,
1750 1758 .cs_files .cs_added, .cs_files .cs_D {
1751 1759 height: 16px;
1752 1760 padding-right: 10px;
1753 1761 margin-top: 7px;
1754 1762 text-align: left;
1755 1763 }
1756 1764
1757 1765 .cs_icon_td {
1758 1766 min-width: 16px;
1759 1767 width: 16px;
1760 1768 }
1761 1769
1762 1770 .pull-request-merge {
1763 1771 border: 1px solid @grey5;
1764 1772 padding: 10px 0px 20px;
1765 1773 margin-top: 10px;
1766 1774 margin-bottom: 20px;
1767 1775 }
1768 1776
1769 1777 .pull-request-merge ul {
1770 1778 padding: 0px 0px;
1771 1779 }
1772 1780
1773 1781 .pull-request-merge li {
1774 1782 list-style-type: none;
1775 1783 }
1776 1784
1777 1785 .pull-request-merge .pull-request-wrap {
1778 1786 height: auto;
1779 1787 padding: 0px 0px;
1780 1788 text-align: right;
1781 1789 }
1782 1790
1783 1791 .pull-request-merge span {
1784 1792 margin-right: 5px;
1785 1793 }
1786 1794
1787 1795 .pull-request-merge-actions {
1788 1796 min-height: 30px;
1789 1797 padding: 0px 0px;
1790 1798 }
1791 1799
1792 1800 .pull-request-merge-info {
1793 1801 padding: 0px 5px 5px 0px;
1794 1802 }
1795 1803
1796 1804 .merge-status {
1797 1805 margin-right: 5px;
1798 1806 }
1799 1807
1800 1808 .merge-message {
1801 1809 font-size: 1.2em
1802 1810 }
1803 1811
1804 1812 .merge-message.success i,
1805 1813 .merge-icon.success i {
1806 1814 color:@alert1;
1807 1815 }
1808 1816
1809 1817 .merge-message.warning i,
1810 1818 .merge-icon.warning i {
1811 1819 color: @alert3;
1812 1820 }
1813 1821
1814 1822 .merge-message.error i,
1815 1823 .merge-icon.error i {
1816 1824 color:@alert2;
1817 1825 }
1818 1826
1819 1827 .pr-versions {
1820 1828 font-size: 1.1em;
1821 1829
1822 1830 table {
1823 1831 padding: 0px 5px;
1824 1832 }
1825 1833
1826 1834 td {
1827 1835 line-height: 15px;
1828 1836 }
1829 1837
1830 1838 .flag_status {
1831 1839 margin: 0;
1832 1840 }
1833 1841
1834 1842 .compare-radio-button {
1835 1843 position: relative;
1836 1844 top: -3px;
1837 1845 }
1838 1846 }
1839 1847
1840 1848
1841 1849 #close_pull_request {
1842 1850 margin-right: 0px;
1843 1851 }
1844 1852
1845 1853 .empty_data {
1846 1854 color: @grey4;
1847 1855 }
1848 1856
1849 1857 #changeset_compare_view_content {
1850 1858 clear: both;
1851 1859 width: 100%;
1852 1860 box-sizing: border-box;
1853 1861 .border-radius(@border-radius);
1854 1862
1855 1863 .help-block {
1856 1864 margin: @padding 0;
1857 1865 color: @text-color;
1858 1866 &.pre-formatting {
1859 1867 white-space: pre;
1860 1868 }
1861 1869 }
1862 1870
1863 1871 .empty_data {
1864 1872 margin: @padding 0;
1865 1873 }
1866 1874
1867 1875 .alert {
1868 1876 margin-bottom: @space;
1869 1877 }
1870 1878 }
1871 1879
1872 1880 .table_disp {
1873 1881 .status {
1874 1882 width: auto;
1875 1883
1876 1884 .flag_status {
1877 1885 float: left;
1878 1886 }
1879 1887 }
1880 1888 }
1881 1889
1882 1890
1883 1891 .creation_in_progress {
1884 1892 color: @grey4
1885 1893 }
1886 1894
1887 1895 .status_box_menu {
1888 1896 margin: 0;
1889 1897 }
1890 1898
1891 1899 .notification-table{
1892 1900 margin-bottom: @space;
1893 1901 display: table;
1894 1902 width: 100%;
1895 1903
1896 1904 .container{
1897 1905 display: table-row;
1898 1906
1899 1907 .notification-header{
1900 1908 border-bottom: @border-thickness solid @border-default-color;
1901 1909 }
1902 1910
1903 1911 .notification-subject{
1904 1912 display: table-cell;
1905 1913 }
1906 1914 }
1907 1915 }
1908 1916
1909 1917 // Notifications
1910 1918 .notification-header{
1911 1919 display: table;
1912 1920 width: 100%;
1913 1921 padding: floor(@basefontsize/2) 0;
1914 1922 line-height: 1em;
1915 1923
1916 1924 .desc, .delete-notifications, .read-notifications{
1917 1925 display: table-cell;
1918 1926 text-align: left;
1919 1927 }
1920 1928
1921 1929 .desc{
1922 1930 width: 1163px;
1923 1931 }
1924 1932
1925 1933 .delete-notifications, .read-notifications{
1926 1934 width: 35px;
1927 1935 min-width: 35px; //fixes when only one button is displayed
1928 1936 }
1929 1937 }
1930 1938
1931 1939 .notification-body {
1932 1940 .markdown-block,
1933 1941 .rst-block {
1934 1942 padding: @padding 0;
1935 1943 }
1936 1944
1937 1945 .notification-subject {
1938 1946 padding: @textmargin 0;
1939 1947 border-bottom: @border-thickness solid @border-default-color;
1940 1948 }
1941 1949 }
1942 1950
1943 1951
1944 1952 .notifications_buttons{
1945 1953 float: right;
1946 1954 }
1947 1955
1948 1956 #notification-status{
1949 1957 display: inline;
1950 1958 }
1951 1959
1952 1960 // Repositories
1953 1961
1954 1962 #summary.fields{
1955 1963 display: table;
1956 1964
1957 1965 .field{
1958 1966 display: table-row;
1959 1967
1960 1968 .label-summary{
1961 1969 display: table-cell;
1962 1970 min-width: @label-summary-minwidth;
1963 1971 padding-top: @padding/2;
1964 1972 padding-bottom: @padding/2;
1965 1973 padding-right: @padding/2;
1966 1974 }
1967 1975
1968 1976 .input{
1969 1977 display: table-cell;
1970 1978 padding: @padding/2;
1971 1979
1972 1980 input{
1973 1981 min-width: 29em;
1974 1982 padding: @padding/4;
1975 1983 }
1976 1984 }
1977 1985 .statistics, .downloads{
1978 1986 .disabled{
1979 1987 color: @grey4;
1980 1988 }
1981 1989 }
1982 1990 }
1983 1991 }
1984 1992
1985 1993 #summary{
1986 1994 width: 70%;
1987 1995 }
1988 1996
1989 1997
1990 1998 // Journal
1991 1999 .journal.title {
1992 2000 h5 {
1993 2001 float: left;
1994 2002 margin: 0;
1995 2003 width: 70%;
1996 2004 }
1997 2005
1998 2006 ul {
1999 2007 float: right;
2000 2008 display: inline-block;
2001 2009 margin: 0;
2002 2010 width: 30%;
2003 2011 text-align: right;
2004 2012
2005 2013 li {
2006 2014 display: inline;
2007 2015 font-size: @journal-fontsize;
2008 2016 line-height: 1em;
2009 2017
2010 2018 list-style-type: none;
2011 2019 }
2012 2020 }
2013 2021 }
2014 2022
2015 2023 .filterexample {
2016 2024 position: absolute;
2017 2025 top: 95px;
2018 2026 left: @contentpadding;
2019 2027 color: @rcblue;
2020 2028 font-size: 11px;
2021 2029 font-family: @text-regular;
2022 2030 cursor: help;
2023 2031
2024 2032 &:hover {
2025 2033 color: @rcdarkblue;
2026 2034 }
2027 2035
2028 2036 @media (max-width:768px) {
2029 2037 position: relative;
2030 2038 top: auto;
2031 2039 left: auto;
2032 2040 display: block;
2033 2041 }
2034 2042 }
2035 2043
2036 2044
2037 2045 #journal{
2038 2046 margin-bottom: @space;
2039 2047
2040 2048 .journal_day{
2041 2049 margin-bottom: @textmargin/2;
2042 2050 padding-bottom: @textmargin/2;
2043 2051 font-size: @journal-fontsize;
2044 2052 border-bottom: @border-thickness solid @border-default-color;
2045 2053 }
2046 2054
2047 2055 .journal_container{
2048 2056 margin-bottom: @space;
2049 2057
2050 2058 .journal_user{
2051 2059 display: inline-block;
2052 2060 }
2053 2061 .journal_action_container{
2054 2062 display: block;
2055 2063 margin-top: @textmargin;
2056 2064
2057 2065 div{
2058 2066 display: inline;
2059 2067 }
2060 2068
2061 2069 div.journal_action_params{
2062 2070 display: block;
2063 2071 }
2064 2072
2065 2073 div.journal_repo:after{
2066 2074 content: "\A";
2067 2075 white-space: pre;
2068 2076 }
2069 2077
2070 2078 div.date{
2071 2079 display: block;
2072 2080 margin-bottom: @textmargin;
2073 2081 }
2074 2082 }
2075 2083 }
2076 2084 }
2077 2085
2078 2086 // Files
2079 2087 .edit-file-title {
2080 2088 font-size: 16px;
2081 2089
2082 2090 .title-heading {
2083 2091 padding: 2px;
2084 2092 }
2085 2093 }
2086 2094
2087 2095 .edit-file-fieldset {
2088 2096 margin: @sidebarpadding 0;
2089 2097
2090 2098 .fieldset {
2091 2099 .left-label {
2092 2100 width: 13%;
2093 2101 }
2094 2102 .right-content {
2095 2103 width: 87%;
2096 2104 max-width: 100%;
2097 2105 }
2098 2106 .filename-label {
2099 2107 margin-top: 13px;
2100 2108 }
2101 2109 .commit-message-label {
2102 2110 margin-top: 4px;
2103 2111 }
2104 2112 .file-upload-input {
2105 2113 input {
2106 2114 display: none;
2107 2115 }
2108 2116 margin-top: 10px;
2109 2117 }
2110 2118 .file-upload-label {
2111 2119 margin-top: 10px;
2112 2120 }
2113 2121 p {
2114 2122 margin-top: 5px;
2115 2123 }
2116 2124
2117 2125 }
2118 2126 .custom-path-link {
2119 2127 margin-left: 5px;
2120 2128 }
2121 2129 #commit {
2122 2130 resize: vertical;
2123 2131 }
2124 2132 }
2125 2133
2126 2134 .delete-file-preview {
2127 2135 max-height: 250px;
2128 2136 }
2129 2137
2130 2138 .new-file,
2131 2139 #filter_activate,
2132 2140 #filter_deactivate {
2133 2141 float: right;
2134 2142 margin: 0 0 0 10px;
2135 2143 }
2136 2144
2137 2145 .file-upload-transaction-wrapper {
2138 2146 margin-top: 57px;
2139 2147 clear: both;
2140 2148 }
2141 2149
2142 2150 .file-upload-transaction-wrapper .error {
2143 2151 color: @color5;
2144 2152 }
2145 2153
2146 2154 .file-upload-transaction {
2147 2155 min-height: 200px;
2148 2156 padding: 54px;
2149 2157 border: 1px solid @grey5;
2150 2158 text-align: center;
2151 2159 clear: both;
2152 2160 }
2153 2161
2154 2162 .file-upload-transaction i {
2155 2163 font-size: 48px
2156 2164 }
2157 2165
2158 2166 h3.files_location{
2159 2167 line-height: 2.4em;
2160 2168 }
2161 2169
2162 2170 .browser-nav {
2163 2171 width: 100%;
2164 2172 display: table;
2165 2173 margin-bottom: 20px;
2166 2174
2167 2175 .info_box {
2168 2176 float: left;
2169 2177 display: inline-table;
2170 2178 height: 2.5em;
2171 2179
2172 2180 .browser-cur-rev, .info_box_elem {
2173 2181 display: table-cell;
2174 2182 vertical-align: middle;
2175 2183 }
2176 2184
2177 2185 .drop-menu {
2178 2186 margin: 0 10px;
2179 2187 }
2180 2188
2181 2189 .info_box_elem {
2182 2190 border-top: @border-thickness solid @grey5;
2183 2191 border-bottom: @border-thickness solid @grey5;
2184 2192 box-shadow: @button-shadow;
2185 2193
2186 2194 #at_rev, a {
2187 2195 padding: 0.6em 0.4em;
2188 2196 margin: 0;
2189 2197 .box-shadow(none);
2190 2198 border: 0;
2191 2199 height: 12px;
2192 2200 color: @grey2;
2193 2201 }
2194 2202
2195 2203 input#at_rev {
2196 2204 max-width: 50px;
2197 2205 text-align: center;
2198 2206 }
2199 2207
2200 2208 &.previous {
2201 2209 border: @border-thickness solid @grey5;
2202 2210 border-top-left-radius: @border-radius;
2203 2211 border-bottom-left-radius: @border-radius;
2204 2212
2205 2213 &:hover {
2206 2214 border-color: @grey4;
2207 2215 }
2208 2216
2209 2217 .disabled {
2210 2218 color: @grey5;
2211 2219 cursor: not-allowed;
2212 2220 opacity: 0.5;
2213 2221 }
2214 2222 }
2215 2223
2216 2224 &.next {
2217 2225 border: @border-thickness solid @grey5;
2218 2226 border-top-right-radius: @border-radius;
2219 2227 border-bottom-right-radius: @border-radius;
2220 2228
2221 2229 &:hover {
2222 2230 border-color: @grey4;
2223 2231 }
2224 2232
2225 2233 .disabled {
2226 2234 color: @grey5;
2227 2235 cursor: not-allowed;
2228 2236 opacity: 0.5;
2229 2237 }
2230 2238 }
2231 2239 }
2232 2240
2233 2241 .browser-cur-rev {
2234 2242
2235 2243 span{
2236 2244 margin: 0;
2237 2245 color: @rcblue;
2238 2246 height: 12px;
2239 2247 display: inline-block;
2240 2248 padding: 0.7em 1em ;
2241 2249 border: @border-thickness solid @rcblue;
2242 2250 margin-right: @padding;
2243 2251 }
2244 2252 }
2245 2253
2246 2254 }
2247 2255
2248 2256 .select-index-number {
2249 2257 margin: 0 0 0 20px;
2250 2258 color: @grey3;
2251 2259 }
2252 2260
2253 2261 .search_activate {
2254 2262 display: table-cell;
2255 2263 vertical-align: middle;
2256 2264
2257 2265 input, label{
2258 2266 margin: 0;
2259 2267 padding: 0;
2260 2268 }
2261 2269
2262 2270 input{
2263 2271 margin-left: @textmargin;
2264 2272 }
2265 2273
2266 2274 }
2267 2275 }
2268 2276
2269 2277 .browser-cur-rev{
2270 2278 margin-bottom: @textmargin;
2271 2279 }
2272 2280
2273 2281 #node_filter_box_loading{
2274 2282 .info_text;
2275 2283 }
2276 2284
2277 2285 .browser-search {
2278 2286 margin: -25px 0px 5px 0px;
2279 2287 }
2280 2288
2281 2289 .files-quick-filter {
2282 2290 float: right;
2283 2291 width: 180px;
2284 2292 position: relative;
2285 2293 }
2286 2294
2287 2295 .files-filter-box {
2288 2296 display: flex;
2289 2297 padding: 0px;
2290 2298 border-radius: 3px;
2291 2299 margin-bottom: 0;
2292 2300
2293 2301 a {
2294 2302 border: none !important;
2295 2303 }
2296 2304
2297 2305 li {
2298 2306 list-style-type: none
2299 2307 }
2300 2308 }
2301 2309
2302 2310 .files-filter-box-path {
2303 2311 line-height: 33px;
2304 2312 padding: 0;
2305 2313 width: 20px;
2306 2314 position: absolute;
2307 2315 z-index: 11;
2308 2316 left: 5px;
2309 2317 }
2310 2318
2311 2319 .files-filter-box-input {
2312 2320 margin-right: 0;
2313 2321
2314 2322 input {
2315 2323 border: 1px solid @white;
2316 2324 padding-left: 25px;
2317 2325 width: 145px;
2318 2326
2319 2327 &:hover {
2320 2328 border-color: @grey6;
2321 2329 }
2322 2330
2323 2331 &:focus {
2324 2332 border-color: @grey5;
2325 2333 }
2326 2334 }
2327 2335 }
2328 2336
2329 2337 .browser-result{
2330 2338 td a{
2331 2339 margin-left: 0.5em;
2332 2340 display: inline-block;
2333 2341
2334 2342 em {
2335 2343 font-weight: @text-bold-weight;
2336 2344 font-family: @text-bold;
2337 2345 }
2338 2346 }
2339 2347 }
2340 2348
2341 2349 .browser-highlight{
2342 2350 background-color: @grey5-alpha;
2343 2351 }
2344 2352
2345 2353
2346 2354 .edit-file-fieldset #location,
2347 2355 .edit-file-fieldset #filename {
2348 2356 display: flex;
2349 2357 width: -moz-available; /* WebKit-based browsers will ignore this. */
2350 2358 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2351 2359 width: fill-available;
2352 2360 border: 0;
2353 2361 }
2354 2362
2355 2363 .path-items {
2356 2364 display: flex;
2357 2365 padding: 0;
2358 2366 border: 1px solid #eeeeee;
2359 2367 width: 100%;
2360 2368 float: left;
2361 2369
2362 2370 .breadcrumb-path {
2363 2371 line-height: 30px;
2364 2372 padding: 0 4px;
2365 2373 white-space: nowrap;
2366 2374 }
2367 2375
2368 2376 .location-path {
2369 2377 width: -moz-available; /* WebKit-based browsers will ignore this. */
2370 2378 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2371 2379 width: fill-available;
2372 2380
2373 2381 .file-name-input {
2374 2382 padding: 0.5em 0;
2375 2383 }
2376 2384
2377 2385 }
2378 2386
2379 2387 ul {
2380 2388 display: flex;
2381 2389 margin: 0;
2382 2390 padding: 0;
2383 2391 width: 100%;
2384 2392 }
2385 2393
2386 2394 li {
2387 2395 list-style-type: none;
2388 2396 }
2389 2397
2390 2398 }
2391 2399
2392 2400 .editor-items {
2393 2401 height: 40px;
2394 2402 margin: 10px 0 -17px 10px;
2395 2403
2396 2404 .editor-action {
2397 2405 cursor: pointer;
2398 2406 }
2399 2407
2400 2408 .editor-action.active {
2401 2409 border-bottom: 2px solid #5C5C5C;
2402 2410 }
2403 2411
2404 2412 li {
2405 2413 list-style-type: none;
2406 2414 }
2407 2415 }
2408 2416
2409 2417 .edit-file-fieldset .message textarea {
2410 2418 border: 1px solid #eeeeee;
2411 2419 }
2412 2420
2413 2421 #files_data .codeblock {
2414 2422 background-color: #F5F5F5;
2415 2423 }
2416 2424
2417 2425 #editor_preview {
2418 2426 background: white;
2419 2427 }
2420 2428
2421 2429 .show-editor {
2422 2430 padding: 10px;
2423 2431 background-color: white;
2424 2432
2425 2433 }
2426 2434
2427 2435 .show-preview {
2428 2436 padding: 10px;
2429 2437 background-color: white;
2430 2438 border-left: 1px solid #eeeeee;
2431 2439 }
2432 2440 // quick filter
2433 2441 .grid-quick-filter {
2434 2442 float: right;
2435 2443 position: relative;
2436 2444 }
2437 2445
2438 2446 .grid-filter-box {
2439 2447 display: flex;
2440 2448 padding: 0px;
2441 2449 border-radius: 3px;
2442 2450 margin-bottom: 0;
2443 2451
2444 2452 a {
2445 2453 border: none !important;
2446 2454 }
2447 2455
2448 2456 li {
2449 2457 list-style-type: none
2450 2458 }
2451 2459 }
2452 2460
2453 2461 .grid-filter-box-icon {
2454 2462 line-height: 33px;
2455 2463 padding: 0;
2456 2464 width: 20px;
2457 2465 position: absolute;
2458 2466 z-index: 11;
2459 2467 left: 5px;
2460 2468 }
2461 2469
2462 2470 .grid-filter-box-input {
2463 2471 margin-right: 0;
2464 2472
2465 2473 input {
2466 2474 border: 1px solid @white;
2467 2475 padding-left: 25px;
2468 2476 width: 145px;
2469 2477
2470 2478 &:hover {
2471 2479 border-color: @grey6;
2472 2480 }
2473 2481
2474 2482 &:focus {
2475 2483 border-color: @grey5;
2476 2484 }
2477 2485 }
2478 2486 }
2479 2487
2480 2488
2481 2489
2482 2490 // Search
2483 2491
2484 2492 .search-form{
2485 2493 #q {
2486 2494 width: @search-form-width;
2487 2495 }
2488 2496 .fields{
2489 2497 margin: 0 0 @space;
2490 2498 }
2491 2499
2492 2500 label{
2493 2501 display: inline-block;
2494 2502 margin-right: @textmargin;
2495 2503 padding-top: 0.25em;
2496 2504 }
2497 2505
2498 2506
2499 2507 .results{
2500 2508 clear: both;
2501 2509 margin: 0 0 @padding;
2502 2510 }
2503 2511
2504 2512 .search-tags {
2505 2513 padding: 5px 0;
2506 2514 }
2507 2515 }
2508 2516
2509 2517 div.search-feedback-items {
2510 2518 display: inline-block;
2511 2519 }
2512 2520
2513 2521 div.search-code-body {
2514 2522 background-color: #ffffff; padding: 5px 0 5px 10px;
2515 2523 pre {
2516 2524 .match { background-color: #faffa6;}
2517 2525 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2518 2526 }
2519 2527 }
2520 2528
2521 2529 .expand_commit.search {
2522 2530 .show_more.open {
2523 2531 height: auto;
2524 2532 max-height: none;
2525 2533 }
2526 2534 }
2527 2535
2528 2536 .search-results {
2529 2537
2530 2538 h2 {
2531 2539 margin-bottom: 0;
2532 2540 }
2533 2541 .codeblock {
2534 2542 border: none;
2535 2543 background: transparent;
2536 2544 }
2537 2545
2538 2546 .codeblock-header {
2539 2547 border: none;
2540 2548 background: transparent;
2541 2549 }
2542 2550
2543 2551 .code-body {
2544 2552 border: @border-thickness solid @grey6;
2545 2553 .border-radius(@border-radius);
2546 2554 }
2547 2555
2548 2556 .td-commit {
2549 2557 &:extend(pre);
2550 2558 border-bottom: @border-thickness solid @border-default-color;
2551 2559 }
2552 2560
2553 2561 .message {
2554 2562 height: auto;
2555 2563 max-width: 350px;
2556 2564 white-space: normal;
2557 2565 text-overflow: initial;
2558 2566 overflow: visible;
2559 2567
2560 2568 .match { background-color: #faffa6;}
2561 2569 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2562 2570 }
2563 2571
2564 2572 .path {
2565 2573 border-bottom: none !important;
2566 2574 border-left: 1px solid @grey6 !important;
2567 2575 border-right: 1px solid @grey6 !important;
2568 2576 }
2569 2577 }
2570 2578
2571 2579 table.rctable td.td-search-results div {
2572 2580 max-width: 100%;
2573 2581 }
2574 2582
2575 2583 #tip-box, .tip-box{
2576 2584 padding: @menupadding/2;
2577 2585 display: block;
2578 2586 border: @border-thickness solid @border-highlight-color;
2579 2587 .border-radius(@border-radius);
2580 2588 background-color: white;
2581 2589 z-index: 99;
2582 2590 white-space: pre-wrap;
2583 2591 }
2584 2592
2585 2593 #linktt {
2586 2594 width: 79px;
2587 2595 }
2588 2596
2589 2597 #help_kb .modal-content{
2590 2598 max-width: 750px;
2591 2599 margin: 10% auto;
2592 2600
2593 2601 table{
2594 2602 td,th{
2595 2603 border-bottom: none;
2596 2604 line-height: 2.5em;
2597 2605 }
2598 2606 th{
2599 2607 padding-bottom: @textmargin/2;
2600 2608 }
2601 2609 td.keys{
2602 2610 text-align: center;
2603 2611 }
2604 2612 }
2605 2613
2606 2614 .block-left{
2607 2615 width: 45%;
2608 2616 margin-right: 5%;
2609 2617 }
2610 2618 .modal-footer{
2611 2619 clear: both;
2612 2620 }
2613 2621 .key.tag{
2614 2622 padding: 0.5em;
2615 2623 background-color: @rcblue;
2616 2624 color: white;
2617 2625 border-color: @rcblue;
2618 2626 .box-shadow(none);
2619 2627 }
2620 2628 }
2621 2629
2622 2630
2623 2631
2624 2632 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2625 2633
2626 2634 @import 'statistics-graph';
2627 2635 @import 'tables';
2628 2636 @import 'forms';
2629 2637 @import 'diff';
2630 2638 @import 'summary';
2631 2639 @import 'navigation';
2632 2640
2633 2641 //--- SHOW/HIDE SECTIONS --//
2634 2642
2635 2643 .btn-collapse {
2636 2644 float: right;
2637 2645 text-align: right;
2638 2646 font-family: @text-light;
2639 2647 font-size: @basefontsize;
2640 2648 cursor: pointer;
2641 2649 border: none;
2642 2650 color: @rcblue;
2643 2651 }
2644 2652
2645 2653 table.rctable,
2646 2654 table.dataTable {
2647 2655 .btn-collapse {
2648 2656 float: right;
2649 2657 text-align: right;
2650 2658 }
2651 2659 }
2652 2660
2653 2661 table.rctable {
2654 2662 &.permissions {
2655 2663
2656 2664 th.td-owner {
2657 2665 padding: 0;
2658 2666 }
2659 2667
2660 2668 th {
2661 2669 font-weight: normal;
2662 2670 padding: 0 5px;
2663 2671 }
2664 2672
2665 2673 }
2666 2674 }
2667 2675
2668 2676
2669 2677 // TODO: johbo: Fix for IE10, this avoids that we see a border
2670 2678 // and padding around checkboxes and radio boxes. Move to the right place,
2671 2679 // or better: Remove this once we did the form refactoring.
2672 2680 input[type=checkbox],
2673 2681 input[type=radio] {
2674 2682 padding: 0;
2675 2683 border: none;
2676 2684 }
2677 2685
2678 2686 .toggle-ajax-spinner{
2679 2687 height: 16px;
2680 2688 width: 16px;
2681 2689 }
2682 2690
2683 2691
2684 2692 .markup-form .clearfix {
2685 2693 .border-radius(@border-radius);
2686 2694 margin: 0px;
2687 2695 }
2688 2696
2689 2697 .markup-form-area {
2690 2698 padding: 8px 12px;
2691 2699 border: 1px solid @grey4;
2692 2700 .border-radius(@border-radius);
2693 2701 }
2694 2702
2695 2703 .markup-form-area-header .nav-links {
2696 2704 display: flex;
2697 2705 flex-flow: row wrap;
2698 2706 -webkit-flex-flow: row wrap;
2699 2707 width: 100%;
2700 2708 }
2701 2709
2702 2710 .markup-form-area-footer {
2703 2711 display: flex;
2704 2712 }
2705 2713
2706 2714 .markup-form-area-footer .toolbar {
2707 2715
2708 2716 }
2709 2717
2710 2718 // markup Form
2711 2719 div.markup-form {
2712 2720 margin-top: 20px;
2713 2721 }
2714 2722
2715 2723 .markup-form strong {
2716 2724 display: block;
2717 2725 margin-bottom: 15px;
2718 2726 }
2719 2727
2720 2728 .markup-form textarea {
2721 2729 width: 100%;
2722 2730 height: 100px;
2723 2731 font-family: @text-monospace;
2724 2732 }
2725 2733
2726 2734 form.markup-form {
2727 2735 margin-top: 10px;
2728 2736 margin-left: 10px;
2729 2737 }
2730 2738
2731 2739 .markup-form .comment-block-ta,
2732 2740 .markup-form .preview-box {
2733 2741 .border-radius(@border-radius);
2734 2742 .box-sizing(border-box);
2735 2743 background-color: white;
2736 2744 }
2737 2745
2738 2746 .markup-form .preview-box.unloaded {
2739 2747 height: 50px;
2740 2748 text-align: center;
2741 2749 padding: 20px;
2742 2750 background-color: white;
2743 2751 }
2744 2752
2745 2753
2746 2754 .dropzone-wrapper {
2747 2755 border: 1px solid @grey5;
2748 2756 padding: 20px;
2749 2757 }
2750 2758
2751 2759 .dropzone,
2752 2760 .dropzone-pure {
2753 2761 border: 2px dashed @grey5;
2754 2762 border-radius: 5px;
2755 2763 background: white;
2756 2764 min-height: 200px;
2757 2765 padding: 54px;
2758 2766
2759 2767 .dz-message {
2760 2768 font-weight: 700;
2761 2769 text-align: center;
2762 2770 margin: 2em 0;
2763 2771 }
2764 2772
2765 2773 }
2766 2774
2767 2775 .dz-preview {
2768 2776 margin: 10px 0 !important;
2769 2777 position: relative;
2770 2778 vertical-align: top;
2771 2779 padding: 10px;
2772 2780 border-bottom: 1px solid @grey5;
2773 2781 }
2774 2782
2775 2783 .dz-filename {
2776 2784 font-weight: 700;
2777 2785 float:left;
2778 2786 }
2779 2787
2780 2788 .dz-sending {
2781 2789 float: right;
2782 2790 }
2783 2791
2784 2792 .dz-response {
2785 2793 clear:both
2786 2794 }
2787 2795
2788 2796 .dz-filename-size {
2789 2797 float:right
2790 2798 }
2791 2799
2792 2800 .dz-error-message {
2793 2801 color: @alert2;
2794 2802 padding-top: 10px;
2795 2803 clear: both;
2796 2804 }
@@ -1,90 +1,93 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 %if c.closed:
6 6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 7 %else:
8 8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 9 %endif
10 10 </div>
11 11 </div>
12 12
13 13 <div class="panel panel-default">
14 14 <div class="panel-heading">
15 15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 16 </div>
17 17 <div class="panel-body panel-body-min-height">
18 18 <table id="pull_request_list_table" class="display"></table>
19 19 </div>
20 20 </div>
21 21
22 22 <script type="text/javascript">
23 23 $(document).ready(function() {
24 24
25 25 $('#show_closed').on('click', function(e){
26 26 if($(this).is(":checked")){
27 27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 28 }
29 29 else{
30 30 window.location = "${h.route_path('my_account_pullrequests')}";
31 31 }
32 32 });
33 33
34 34 var $pullRequestListTable = $('#pull_request_list_table');
35 35
36 36 // participating object list
37 37 $pullRequestListTable.DataTable({
38 38 processing: true,
39 39 serverSide: true,
40 40 ajax: {
41 41 "url": "${h.route_path('my_account_pullrequests_data')}",
42 42 "data": function (d) {
43 43 d.closed = "${c.closed}";
44 44 }
45 45 },
46 46 dom: 'rtp',
47 47 pageLength: ${c.visual.dashboard_items},
48 48 order: [[ 2, "desc" ]],
49 49 columns: [
50 50 { data: {"_": "status",
51 51 "sort": "status"}, title: "", className: "td-status", orderable: false},
52 52 { data: {"_": "target_repo",
53 53 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
54 54 { data: {"_": "name",
55 55 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
56 56 { data: {"_": "author",
57 57 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
58 58 { data: {"_": "title",
59 59 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
60 60 { data: {"_": "comments",
61 61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
62 62 { data: {"_": "updated_on",
63 63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
64 64 ],
65 65 language: {
66 66 paginate: DEFAULT_GRID_PAGINATION,
67 67 sProcessing: _gettext('loading...'),
68 68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 69 },
70 70 "drawCallback": function( settings, json ) {
71 71 timeagoActivate();
72 72 },
73 73 "createdRow": function ( row, data, index ) {
74 74 if (data['closed']) {
75 75 $(row).addClass('closed');
76 76 }
77 77 if (data['owned']) {
78 78 $(row).addClass('owned');
79 79 }
80 if (data['state'] !== 'created') {
81 $(row).addClass('state-' + data['state']);
82 }
80 83 }
81 84 });
82 85 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
83 86 $pullRequestListTable.css('opacity', 1);
84 87 });
85 88
86 89 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
87 90 $pullRequestListTable.css('opacity', 0.3);
88 91 });
89 92 });
90 93 </script>
@@ -1,117 +1,120 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Requests') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='showpullrequest')}
19 19 </%def>
20 20
21 21
22 22 <%def name="main()">
23 23
24 24 <div class="box">
25 25 <div class="title">
26 26 <ul class="button-links">
27 27 <li class="btn ${('active' if c.active=='open' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 28 <li class="btn ${('active' if c.active=='my' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 29 <li class="btn ${('active' if c.active=='awaiting' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 30 <li class="btn ${('active' if c.active=='awaiting_my' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 31 <li class="btn ${('active' if c.active=='closed' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 32 <li class="btn ${('active' if c.active=='source' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 33 </ul>
34 34
35 35 <ul class="links">
36 36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 37 <li>
38 38 <span>
39 39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 40 ${_('Open new Pull Request')}
41 41 </a>
42 42 </span>
43 43 </li>
44 44 % endif
45 45 </ul>
46 46
47 47 </div>
48 48
49 49 <div class="main-content-full-width">
50 50 <table id="pull_request_list_table" class="display"></table>
51 51 </div>
52 52
53 53 </div>
54 54
55 55 <script type="text/javascript">
56 56 $(document).ready(function() {
57 57
58 58 var $pullRequestListTable = $('#pull_request_list_table');
59 59
60 60 // object list
61 61 $pullRequestListTable.DataTable({
62 62 processing: true,
63 63 serverSide: true,
64 64 ajax: {
65 65 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
66 66 "data": function (d) {
67 67 d.source = "${c.source}";
68 68 d.closed = "${c.closed}";
69 69 d.my = "${c.my}";
70 70 d.awaiting_review = "${c.awaiting_review}";
71 71 d.awaiting_my_review = "${c.awaiting_my_review}";
72 72 }
73 73 },
74 74 dom: 'rtp',
75 75 pageLength: ${c.visual.dashboard_items},
76 76 order: [[ 1, "desc" ]],
77 77 columns: [
78 78 { data: {"_": "status",
79 79 "sort": "status"}, title: "", className: "td-status", orderable: false},
80 80 { data: {"_": "name",
81 81 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
82 82 { data: {"_": "author",
83 83 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
84 84 { data: {"_": "title",
85 85 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
86 86 { data: {"_": "comments",
87 87 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
88 88 { data: {"_": "updated_on",
89 89 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
90 90 ],
91 91 language: {
92 92 paginate: DEFAULT_GRID_PAGINATION,
93 93 sProcessing: _gettext('loading...'),
94 94 emptyTable: _gettext("No pull requests available yet.")
95 95 },
96 96 "drawCallback": function( settings, json ) {
97 97 timeagoActivate();
98 98 },
99 99 "createdRow": function ( row, data, index ) {
100 100 if (data['closed']) {
101 101 $(row).addClass('closed');
102 102 }
103 if (data['state'] !== 'created') {
104 $(row).addClass('state-' + data['state']);
105 }
103 106 }
104 107 });
105 108
106 109 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
107 110 $pullRequestListTable.css('opacity', 1);
108 111 });
109 112
110 113 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
111 114 $pullRequestListTable.css('opacity', 0.3);
112 115 });
113 116
114 117 });
115 118
116 119 </script>
117 120 </%def>
General Comments 0
You need to be logged in to leave comments. Login now